-
-
\ No newline at end of file
diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss
index 04243e0775df..a8ae53908b05 100644
--- a/src/lib/slider/slider.scss
+++ b/src/lib/slider/slider.scss
@@ -7,148 +7,135 @@ $md-slider-thickness: 48px !default;
$md-slider-min-size: 128px !default;
$md-slider-padding: 8px !default;
-$md-slider-track-height: 2px !default;
+$md-slider-track-thickness: 2px !default;
$md-slider-thumb-size: 20px !default;
$md-slider-thumb-default-scale: 0.7 !default;
$md-slider-thumb-focus-scale: 1 !default;
-$md-slider-thumb-arrow-height: 16px !default;
-$md-slider-thumb-arrow-width: 28px !default;
+$md-slider-thumb-arrow-gap: 12px !default;
$md-slider-thumb-label-size: 28px !default;
-// The thumb has to be moved down so that it appears right over the slider track when visible and
-// on the slider track when not.
-$md-slider-thumb-label-top: ($md-slider-thickness / 2) -
- ($md-slider-thumb-default-scale * $md-slider-thumb-size / 2) - $md-slider-thumb-label-size -
- $md-slider-thumb-arrow-height + 10px !default;
-// Uses a container height and an item height to center an item vertically within the container.
-@function center-vertically($containerHeight, $itemHeight) {
- @return ($containerHeight / 2) - ($itemHeight / 2);
-}
+$md-slider-tick-color: rgba(0, 0, 0, 0.6) !default;
+$md-slider-tick-size: 2px !default;
-// Positions the thumb based on its width and height.
-@mixin slider-thumb-position($width: $md-slider-thumb-size, $height: $md-slider-thumb-size) {
- position: absolute;
- top: center-vertically($md-slider-thickness, $height);
- // This makes it so that the center of the thumb aligns with where the click was.
- // This is not affected by the movement of the thumb.
- left: (-$width / 2);
- width: $width;
- height: $height;
- border-radius: max($width, $height);
-}
md-slider {
+ display: inline-block;
+ box-sizing: border-box;
+ position: relative;
height: $md-slider-thickness;
min-width: $md-slider-min-size;
- position: relative;
- padding: 0;
- display: inline-block;
+ padding: $md-slider-padding;
outline: none;
vertical-align: middle;
}
-md-slider *,
-md-slider *::after {
- box-sizing: border-box;
+.md-slider-track {
+ display: flex;
+ flex-grow: 1;
+ align-items: center;
+ position: relative;
+ top: ($md-slider-thickness - $md-slider-track-thickness) / 2 - $md-slider-padding;
+ height: $md-slider-track-thickness;
+ transition: box-shadow $swift-ease-out-duration $swift-ease-out-timing-function;
}
-// Exists in order to pad the slider and keep everything positioned correctly.
-// Cannot be merged with the .md-slider-container.
-.md-slider-wrapper {
- width: 100%;
- height: 100%;
- padding-left: $md-slider-padding;
- padding-right: $md-slider-padding;
+.md-slider-has-ticks.md-slider-active .md-slider-track,
+.md-slider-has-ticks:hover .md-slider-track {
+ box-shadow: inset (-2 * $md-slider-tick-size) 0 0 (-$md-slider-tick-size) $md-slider-tick-color;
}
-
-// Holds the isActive and isSliding classes as well as helps with positioning the children.
-// Cannot be merged with .md-slider-wrapper.
-.md-slider-container {
- position: relative;
+.md-slider-track-fill {
+ flex: 0 0 50%;
+ height: $md-slider-track-thickness;
+ transition: flex-basis $swift-ease-out-duration $swift-ease-out-timing-function;
}
-.md-slider-track-container {
- width: 100%;
- position: absolute;
- top: center-vertically($md-slider-thickness, $md-slider-track-height);
- height: $md-slider-track-height;
+.md-slider-sliding .md-slider-track-fill {
+ transition: none;
}
-.md-slider-track {
+.md-slider-ticks-container {
position: absolute;
left: 0;
- right: 0;
- height: 100%;
+ top: 0;
+ height: $md-slider-track-thickness;
+ width: 100%;
+ overflow: hidden;
}
-.md-slider-track-fill {
- transition-duration: $swift-ease-out-duration;
- transition-timing-function: $swift-ease-out-timing-function;
- transition-property: width, height;
+.md-slider-ticks {
+ background: repeating-linear-gradient(to right, $md-slider-tick-color,
+ $md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;
+ // Firefox doesn't draw the gradient correctly with 'to right'
+ // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1314319).
+ background: -moz-repeating-linear-gradient(0.0001deg, $md-slider-tick-color,
+ $md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;
+ height: $md-slider-track-thickness;
+ width: 100%;
+ opacity: 0;
+ transition: opacity $swift-ease-out-duration $swift-ease-out-timing-function;
}
-.md-slider-tick-container, .md-slider-last-tick-container {
- position: absolute;
- left: 0;
- right: 0;
- height: 100%;
+.md-slider-has-ticks.md-slider-active .md-slider-ticks,
+.md-slider-has-ticks:hover .md-slider-ticks {
+ opacity: 1;
}
.md-slider-thumb-container {
- position: absolute;
- left: 0;
- top: 50%;
- transform: translate3d(-50%, -50%, 0);
- transition-duration: $swift-ease-out-duration;
- transition-timing-function: $swift-ease-out-timing-function;
- transition-property: left, bottom;
-}
-
-.md-slider-thumb-position {
- transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
+ flex: 0 0 auto;
+ position: relative;
+ width: 0;
+ height: 0;
}
.md-slider-thumb {
- z-index: 1;
-
- @include slider-thumb-position($md-slider-thumb-size, $md-slider-thumb-size);
+ position: absolute;
+ left: -$md-slider-thumb-size / 2;
+ top: -$md-slider-thumb-size / 2;
+ width: $md-slider-thumb-size;
+ height: $md-slider-thumb-size;
+ border-radius: 50%;
+ transform-origin: 50% 50%;
transform: scale($md-slider-thumb-default-scale);
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
}
-.md-slider-thumb::after {
- content: '';
- position: absolute;
- width: $md-slider-thumb-size;
- height: $md-slider-thumb-size;
- border-radius: max($md-slider-thumb-size, $md-slider-thumb-size);
- // Separate border properties because, if you combine them into "border", it defaults to 'black'.
- border-width: 3px;
- border-style: solid;
- transition: inherit;
+.md-slider-active .md-slider-thumb {
+ transform: scale($md-slider-thumb-focus-scale);
+}
+
+.md-slider-active.md-slider-thumb-label-showing .md-slider-thumb {
+ transform: scale(0);
}
.md-slider-thumb-label {
display: flex;
align-items: center;
justify-content: center;
-
position: absolute;
- left: -($md-slider-thumb-label-size / 2);
- top: $md-slider-thumb-label-top;
+ left: -$md-slider-thumb-label-size / 2;
+ top: -($md-slider-thumb-label-size + $md-slider-thumb-arrow-gap);
width: $md-slider-thumb-label-size;
height: $md-slider-thumb-label-size;
border-radius: 50%;
-
- transform: scale(0.4) translate3d(0, (-$md-slider-thumb-label-top + 10) / 0.4, 0) rotate(45deg);
+ transform: translateY($md-slider-thumb-label-size / 2 + $md-slider-thumb-arrow-gap)
+ scale(0.4) rotate(45deg);
transition: 300ms $swift-ease-in-out-timing-function;
transition-property: transform, border-radius;
}
+.md-slider-active .md-slider-thumb-label {
+ border-radius: 50% 50% 0;
+ transform: rotate(45deg);
+}
+
+md-slider:not(.md-slider-thumb-label-showing) .md-slider-thumb-label {
+ display: none;
+}
+
.md-slider-thumb-label-text {
z-index: 1;
font-size: 12px;
@@ -158,29 +145,6 @@ md-slider *::after {
transition: opacity 300ms $swift-ease-in-out-timing-function;
}
-.md-slider-container:not(.md-slider-thumb-label-showing) .md-slider-thumb-label {
- display: none;
-}
-
-.md-slider-active.md-slider-thumb-label-showing .md-slider-thumb {
- transform: scale(0);
-}
-
-.md-slider-sliding .md-slider-thumb-position,
-.md-slider-sliding .md-slider-track-fill {
- transition: none;
- cursor: default;
-}
-
-.md-slider-active .md-slider-thumb {
- transform: scale($md-slider-thumb-focus-scale);
-}
-
-.md-slider-active .md-slider-thumb-label {
- border-radius: 50% 50% 0;
- transform: rotate(45deg);
-}
-
.md-slider-active .md-slider-thumb-label-text {
opacity: 1;
}
diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts
index 32dfe87a398c..320b54a9e5b2 100644
--- a/src/lib/slider/slider.spec.ts
+++ b/src/lib/slider/slider.spec.ts
@@ -1,6 +1,6 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ReactiveFormsModule, FormControl} from '@angular/forms';
-import {Component, DebugElement, ViewEncapsulation} from '@angular/core';
+import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdSlider, MdSliderModule} from './slider';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
@@ -45,11 +45,7 @@ describe('MdSlider', () => {
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let trackFillElement: HTMLElement;
- let trackFillDimensions: ClientRect;
let sliderTrackElement: HTMLElement;
- let sliderDimensions: ClientRect;
- let thumbElement: HTMLElement;
- let thumbDimensions: ClientRect;
beforeEach(() => {
fixture = TestBed.createComponent(StandardSlider);
@@ -60,12 +56,7 @@ describe('MdSlider', () => {
sliderInstance = sliderDebugElement.componentInstance;
trackFillElement =
sliderNativeElement.querySelector('.md-slider-track-fill');
- trackFillDimensions = trackFillElement.getBoundingClientRect();
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
- sliderDimensions = sliderTrackElement.getBoundingClientRect();
-
- thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
- thumbDimensions = thumbElement.getBoundingClientRect();
});
it('should set the default values', () => {
@@ -76,118 +67,88 @@ describe('MdSlider', () => {
it('should update the value on a click', () => {
expect(sliderInstance.value).toBe(0);
- dispatchClickEvent(sliderNativeElement, 0.19);
- // The expected value is 19 from: percentage * difference of max and min.
+
+ dispatchClickEventSequence(sliderNativeElement, 0.19);
+
expect(sliderInstance.value).toBe(19);
});
it('should update the value on a slide', () => {
expect(sliderInstance.value).toBe(0);
+
dispatchSlideEventSequence(sliderNativeElement, 0, 0.89, gestureConfig);
- // The expected value is 89 from: percentage * difference of max and min.
+
expect(sliderInstance.value).toBe(89);
});
it('should set the value as min when sliding before the track', () => {
expect(sliderInstance.value).toBe(0);
+
dispatchSlideEventSequence(sliderNativeElement, 0, -1.33, gestureConfig);
+
expect(sliderInstance.value).toBe(0);
});
it('should set the value as max when sliding past the track', () => {
expect(sliderInstance.value).toBe(0);
+
dispatchSlideEventSequence(sliderNativeElement, 0, 1.75, gestureConfig);
+
expect(sliderInstance.value).toBe(100);
});
it('should update the track fill on click', () => {
- expect(trackFillDimensions.width).toBe(0);
- dispatchClickEvent(sliderNativeElement, 0.39);
-
- trackFillDimensions = trackFillElement.getBoundingClientRect();
- thumbDimensions = thumbElement.getBoundingClientRect();
+ expect(trackFillElement.style.flexBasis).toBe('0%');
- // The thumb and track fill positions are relative to the viewport, so to get the thumb's
- // offset relative to the track, subtract the offset on the track fill.
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
- // The track fill width should be equal to the thumb's position.
- expect(trackFillDimensions.width).toBe(thumbPosition);
- });
-
- it('should update the thumb position on click', () => {
- expect(thumbDimensions.left).toBe(sliderDimensions.left);
- // 50% is used here because the click event that is dispatched truncates the position and so
- // a value had to be used that would not be truncated.
- dispatchClickEvent(sliderNativeElement, 0.5);
+ dispatchClickEventSequence(sliderNativeElement, 0.39);
+ fixture.detectChanges();
- thumbDimensions = thumbElement.getBoundingClientRect();
- // The thumb position should be at 50% of the slider's width + the offset of the slider.
- // Both the thumb and the slider are affected by this offset.
- expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left);
+ expect(trackFillElement.style.flexBasis).toBe('39%');
});
it('should update the track fill on slide', () => {
- expect(trackFillDimensions.width).toBe(0);
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.86, gestureConfig);
-
- trackFillDimensions = trackFillElement.getBoundingClientRect();
- thumbDimensions = thumbElement.getBoundingClientRect();
+ expect(trackFillElement.style.flexBasis).toBe('0%');
- // The thumb and track fill positions are relative to the viewport, so to get the thumb's
- // offset relative to the track, subtract the offset on the track fill.
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
- // The track fill width should be equal to the thumb's position.
- expect(trackFillDimensions.width).toBe(thumbPosition);
- });
-
- it('should update the thumb position on slide', () => {
- expect(thumbDimensions.left).toBe(sliderDimensions.left);
- // The slide event also truncates the position passed in, so 50% is used here as well to
- // ensure the ability to calculate the expected position.
- dispatchSlideEventSequence(sliderNativeElement, 0, 0.5, gestureConfig);
+ dispatchSlideEventSequence(sliderNativeElement, 0, 0.86, gestureConfig);
+ fixture.detectChanges();
- thumbDimensions = thumbElement.getBoundingClientRect();
- expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left);
+ expect(trackFillElement.style.flexBasis).toBe('86%');
});
it('should add the md-slider-active class on click', () => {
- let containerElement = sliderNativeElement.querySelector('.md-slider-container');
- expect(containerElement.classList).not.toContain('md-slider-active');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-active');
- dispatchClickEvent(sliderNativeElement, 0.23);
+ dispatchClickEventSequence(sliderNativeElement, 0.23);
fixture.detectChanges();
- expect(containerElement.classList).toContain('md-slider-active');
+ expect(sliderNativeElement.classList).toContain('md-slider-active');
});
it('should remove the md-slider-active class on blur', () => {
- let containerElement = sliderNativeElement.querySelector('.md-slider-container');
-
- dispatchClickEvent(sliderNativeElement, 0.95);
+ dispatchClickEventSequence(sliderNativeElement, 0.95);
fixture.detectChanges();
- expect(containerElement.classList).toContain('md-slider-active');
+ expect(sliderNativeElement.classList).toContain('md-slider-active');
// Call the `onBlur` handler directly because we cannot simulate a focus event in unit tests.
- sliderInstance.onBlur();
+ sliderInstance._onBlur();
fixture.detectChanges();
- expect(containerElement.classList).not.toContain('md-slider-active');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-active');
});
it('should add and remove the md-slider-sliding class when sliding', () => {
- let containerElement = sliderNativeElement.querySelector('.md-slider-container');
- expect(containerElement.classList).not.toContain('md-slider-sliding');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-sliding');
dispatchSlideStartEvent(sliderNativeElement, 0, gestureConfig);
fixture.detectChanges();
- expect(containerElement.classList).toContain('md-slider-sliding');
+ expect(sliderNativeElement.classList).toContain('md-slider-sliding');
dispatchSlideEndEvent(sliderNativeElement, 0.34, gestureConfig);
fixture.detectChanges();
- expect(containerElement.classList).not.toContain('md-slider-sliding');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-sliding');
});
});
@@ -214,34 +175,36 @@ describe('MdSlider', () => {
it('should not change the value on click when disabled', () => {
expect(sliderInstance.value).toBe(0);
- dispatchClickEvent(sliderNativeElement, 0.63);
+
+ dispatchClickEventSequence(sliderNativeElement, 0.63);
+
expect(sliderInstance.value).toBe(0);
});
it('should not change the value on slide when disabled', () => {
expect(sliderInstance.value).toBe(0);
+
dispatchSlideEventSequence(sliderNativeElement, 0, 0.5, gestureConfig);
+
expect(sliderInstance.value).toBe(0);
});
it('should not add the md-slider-active class on click when disabled', () => {
- let containerElement = sliderNativeElement.querySelector('.md-slider-container');
- expect(containerElement.classList).not.toContain('md-slider-active');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-active');
- dispatchClickEvent(sliderNativeElement, 0.43);
+ dispatchClickEventSequence(sliderNativeElement, 0.43);
fixture.detectChanges();
- expect(containerElement.classList).not.toContain('md-slider-active');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-active');
});
it('should not add the md-slider-sliding class on slide when disabled', () => {
- let containerElement = sliderNativeElement.querySelector('.md-slider-container');
- expect(containerElement.classList).not.toContain('md-slider-sliding');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-sliding');
dispatchSlideStartEvent(sliderNativeElement, 0.46, gestureConfig);
fixture.detectChanges();
- expect(containerElement.classList).not.toContain('md-slider-sliding');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-sliding');
});
});
@@ -251,10 +214,9 @@ describe('MdSlider', () => {
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
- let sliderDimensions: ClientRect;
let trackFillElement: HTMLElement;
- let thumbElement: HTMLElement;
- let tickContainerElement: HTMLElement;
+ let ticksContainerElement: HTMLElement;
+ let ticksElement: HTMLElement;
let testComponent: SliderWithMinAndMax;
beforeEach(() => {
@@ -266,11 +228,10 @@ describe('MdSlider', () => {
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.injector.get(MdSlider);
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
- sliderDimensions = sliderTrackElement.getBoundingClientRect();
trackFillElement = sliderNativeElement.querySelector('.md-slider-track-fill');
- thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
- tickContainerElement =
- sliderNativeElement.querySelector('.md-slider-tick-container');
+ ticksContainerElement =
+ sliderNativeElement.querySelector('.md-slider-ticks-container');
+ ticksElement = sliderNativeElement.querySelector('.md-slider-ticks');
});
it('should set the default values from the attributes', () => {
@@ -280,7 +241,9 @@ describe('MdSlider', () => {
});
it('should set the correct value on click', () => {
- dispatchClickEvent(sliderNativeElement, 0.09);
+ dispatchClickEventSequence(sliderNativeElement, 0.09);
+ fixture.detectChanges();
+
// Computed by multiplying the difference between the min and the max by the percentage from
// the click and adding that to the minimum.
let value = Math.round(4 + (0.09 * (6 - 4)));
@@ -289,69 +252,59 @@ describe('MdSlider', () => {
it('should set the correct value on slide', () => {
dispatchSlideEventSequence(sliderNativeElement, 0, 0.62, gestureConfig);
+ fixture.detectChanges();
+
// Computed by multiplying the difference between the min and the max by the percentage from
// the click and adding that to the minimum.
let value = Math.round(4 + (0.62 * (6 - 4)));
expect(sliderInstance.value).toBe(value);
});
- it('should snap the thumb and fill to the nearest value on click', () => {
- dispatchClickEvent(sliderNativeElement, 0.68);
+ it('should snap the fill to the nearest value on click', () => {
+ dispatchClickEventSequence(sliderNativeElement, 0.68);
fixture.detectChanges();
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let thumbDimensions = thumbElement.getBoundingClientRect();
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
-
// The closest snap is halfway on the slider.
- expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left);
- expect(trackFillDimensions.width).toBe(thumbPosition);
+ expect(trackFillElement.style.flexBasis).toBe('50%');
});
- it('should snap the thumb and fill to the nearest value on slide', () => {
+ it('should snap the fill to the nearest value on slide', () => {
dispatchSlideEventSequence(sliderNativeElement, 0, 0.74, gestureConfig);
fixture.detectChanges();
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let thumbDimensions = thumbElement.getBoundingClientRect();
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
-
// The closest snap is at the halfway point on the slider.
- expect(thumbDimensions.left).toBe(sliderDimensions.left + sliderDimensions.width * 0.5);
- expect(trackFillDimensions.width).toBe(thumbPosition);
+ expect(trackFillElement.style.flexBasis).toBe('50%');
});
- it('should adjust thumb and ticks when min changes', () => {
+ it('should adjust fill and ticks on mouse enter when min changes', () => {
testComponent.min = -2;
fixture.detectChanges();
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let tickContainerDimensions = tickContainerElement.getBoundingClientRect();
+ dispatchMouseenterEvent(sliderNativeElement);
+ fixture.detectChanges();
- expect(trackFillDimensions.width).toBe(sliderDimensions.width * 6 / 8);
- expect(tickContainerDimensions.width)
- .toBe(sliderDimensions.width - sliderDimensions.width * 6 / 8);
- expect(tickContainerElement.style.background)
- .toContain(`repeating-linear-gradient(to right, black, black 2px, transparent 2px, ` +
- `transparent ${sliderDimensions.width * 6 / 8 - 1}px)`);
+ expect(trackFillElement.style.flexBasis).toBe('75%');
+ expect(ticksElement.style.backgroundSize).toBe('75% 2px');
+ // Make sure it cuts off the last half tick interval.
+ expect(ticksElement.style.marginLeft).toBe('37.5%');
+ expect(ticksContainerElement.style.marginLeft).toBe('-37.5%');
});
- it('should adjust thumb and ticks when max changes', () => {
+ it('should adjust fill and ticks on mouse enter when max changes', () => {
testComponent.min = -2;
fixture.detectChanges();
testComponent.max = 10;
fixture.detectChanges();
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let tickContainerDimensions = tickContainerElement.getBoundingClientRect();
+ dispatchMouseenterEvent(sliderNativeElement);
+ fixture.detectChanges();
- expect(trackFillDimensions.width).toBe(sliderDimensions.width * 6 / 12);
- expect(tickContainerDimensions.width)
- .toBe(sliderDimensions.width - sliderDimensions.width * 6 / 12);
- expect(tickContainerElement.style.background)
- .toContain(`repeating-linear-gradient(to right, black, black 2px, transparent 2px, ` +
- `transparent ${sliderDimensions.width * 6 / 12 - 1}px)`);
+ expect(trackFillElement.style.flexBasis).toBe('50%');
+ expect(ticksElement.style.backgroundSize).toBe('50% 2px');
+ // Make sure it cuts off the last half tick interval.
+ expect(ticksElement.style.marginLeft).toBe('25%');
+ expect(ticksContainerElement.style.marginLeft).toBe('-25%');
});
});
@@ -377,7 +330,9 @@ describe('MdSlider', () => {
});
it('should set the correct value on click', () => {
- dispatchClickEvent(sliderNativeElement, 0.92);
+ dispatchClickEventSequence(sliderNativeElement, 0.92);
+ fixture.detectChanges();
+
// On a slider with default max and min the value should be approximately equal to the
// percentage clicked. This should be the case regardless of what the original set value was.
expect(sliderInstance.value).toBe(92);
@@ -385,6 +340,8 @@ describe('MdSlider', () => {
it('should set the correct value on slide', () => {
dispatchSlideEventSequence(sliderNativeElement, 0, 0.32, gestureConfig);
+ fixture.detectChanges();
+
expect(sliderInstance.value).toBe(32);
});
});
@@ -395,9 +352,7 @@ describe('MdSlider', () => {
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
- let sliderDimensions: ClientRect;
let trackFillElement: HTMLElement;
- let thumbElement: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SliderWithStep);
@@ -407,31 +362,24 @@ describe('MdSlider', () => {
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.injector.get(MdSlider);
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
- sliderDimensions = sliderTrackElement.getBoundingClientRect();
trackFillElement = sliderNativeElement.querySelector('.md-slider-track-fill');
- thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
});
it('should set the correct step value on click', () => {
expect(sliderInstance.value).toBe(0);
- dispatchClickEvent(sliderNativeElement, 0.13);
+ dispatchClickEventSequence(sliderNativeElement, 0.13);
fixture.detectChanges();
expect(sliderInstance.value).toBe(25);
});
- it('should snap the thumb and fill to a step on click', () => {
- dispatchClickEvent(sliderNativeElement, 0.66);
+ it('should snap the fill to a step on click', () => {
+ dispatchClickEventSequence(sliderNativeElement, 0.66);
fixture.detectChanges();
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let thumbDimensions = thumbElement.getBoundingClientRect();
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
-
// The closest step is at 75% of the slider.
- expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.75 + sliderDimensions.left);
- expect(trackFillDimensions.width).toBe(thumbPosition);
+ expect(trackFillElement.style.flexBasis).toBe('75%');
});
it('should set the correct step value on slide', () => {
@@ -445,13 +393,8 @@ describe('MdSlider', () => {
dispatchSlideEventSequence(sliderNativeElement, 0, 0.88, gestureConfig);
fixture.detectChanges();
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let thumbDimensions = thumbElement.getBoundingClientRect();
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
-
// The closest snap is at the end of the slider.
- expect(thumbDimensions.left).toBe(sliderDimensions.width + sliderDimensions.left);
- expect(trackFillDimensions.width).toBe(thumbPosition);
+ expect(trackFillElement.style.flexBasis).toBe('100%');
});
});
@@ -459,8 +402,8 @@ describe('MdSlider', () => {
let fixture: ComponentFixture;
let sliderDebugElement: DebugElement;
let sliderNativeElement: HTMLElement;
- let tickContainer: HTMLElement;
- let lastTickContainer: HTMLElement;
+ let ticksContainerElement: HTMLElement;
+ let ticksElement: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SliderWithAutoTickInterval);
@@ -468,34 +411,20 @@ describe('MdSlider', () => {
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
sliderNativeElement = sliderDebugElement.nativeElement;
- tickContainer = sliderNativeElement.querySelector('.md-slider-tick-container');
- lastTickContainer =
- sliderNativeElement.querySelector('.md-slider-last-tick-container');
+ ticksContainerElement =
+ sliderNativeElement.querySelector('.md-slider-ticks-container');
+ ticksElement = sliderNativeElement.querySelector('.md-slider-ticks');
});
- it('should set the correct tick separation', () => {
- // The first tick mark is going to be at value 30 as it is the first step after 30px. The
- // width of the slider is 112px because the minimum width is 128px with padding of 8px on
- // both sides. The value 30 will be located at the position 33.6px, and 1px is removed from
- // the tick mark location in order to center the tick. Therefore, the tick separation should
- // be 32.6px.
- // toContain is used rather than toBe because FireFox adds 'transparent' to the beginning
- // of the background before the repeating linear gradient.
- expect(tickContainer.style.background).toContain('repeating-linear-gradient(to right, ' +
- 'black, black 2px, transparent 2px, transparent 32.6px)');
- });
-
- it('should draw a tick mark on the end of the track', () => {
- expect(lastTickContainer.style.background).toContain('linear-gradient(to left, black, black' +
- ' 2px, transparent 2px, transparent)');
- });
+ it('should set the correct tick separation on mouse enter', () => {
+ dispatchMouseenterEvent(sliderNativeElement);
+ fixture.detectChanges();
- it('should not draw the second to last tick when it is too close to the last tick', () => {
- // When the second to last tick is too close (less than half the tick separation) to the last
- // one, the tick container width is cut by the tick separation, which removes the second to
- // last tick. Since the width of the slider is 112px and the tick separation is 33.6px, the
- // tick container width should be 78.4px (112 - 33.6).
- expect(tickContainer.style.width).toBe('78.4px');
+ // Ticks should be 30px apart (therefore 30% for a 100px long slider).
+ expect(ticksElement.style.backgroundSize).toBe('30% 2px');
+ // Make sure it cuts off the last half tick interval.
+ expect(ticksElement.style.marginLeft).toBe('15%');
+ expect(ticksContainerElement.style.marginLeft).toBe('-15%');
});
});
@@ -503,8 +432,8 @@ describe('MdSlider', () => {
let fixture: ComponentFixture;
let sliderDebugElement: DebugElement;
let sliderNativeElement: HTMLElement;
- let tickContainer: HTMLElement;
- let lastTickContainer: HTMLElement;
+ let ticksContainerElement: HTMLElement;
+ let ticksElement: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SliderWithSetTickInterval);
@@ -512,22 +441,21 @@ describe('MdSlider', () => {
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
sliderNativeElement = sliderDebugElement.nativeElement;
- tickContainer = sliderNativeElement.querySelector('.md-slider-tick-container');
- lastTickContainer =
- sliderNativeElement.querySelector('.md-slider-last-tick-container');
+ ticksContainerElement =
+ sliderNativeElement.querySelector('.md-slider-ticks-container');
+ ticksElement = sliderNativeElement.querySelector('.md-slider-ticks');
});
- it('should set the correct tick separation', () => {
- // The slider width is 112px, the first step is at value 18 (step of 3 * tick interval of 6),
- // which is at the position 20.16px and 1px is subtracted to center, giving a tick
- // separation of 19.16px.
- expect(tickContainer.style.background).toContain('repeating-linear-gradient(to right, ' +
- 'black, black 2px, transparent 2px, transparent 19.16px)');
- });
+ it('should set the correct tick separation on mouse enter', () => {
+ dispatchMouseenterEvent(sliderNativeElement);
+ fixture.detectChanges();
- it('should draw a tick mark on the end of the track', () => {
- expect(lastTickContainer.style.background).toContain('linear-gradient(to left, black, '
- + 'black 2px, transparent 2px, transparent)');
+ // Ticks should be every 18 values (tickInterval of 6 * step size of 3). On a slider 100px
+ // long with 100 values, this is 18%.
+ expect(ticksElement.style.backgroundSize).toBe('18% 2px');
+ // Make sure it cuts off the last half tick interval.
+ expect(ticksElement.style.marginLeft).toBe('9%');
+ expect(ticksContainerElement.style.marginLeft).toBe('-9%');
});
});
@@ -537,7 +465,6 @@ describe('MdSlider', () => {
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
- let sliderContainerElement: Element;
let thumbLabelTextElement: Element;
beforeEach(() => {
@@ -548,18 +475,17 @@ describe('MdSlider', () => {
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.componentInstance;
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
- sliderContainerElement = sliderNativeElement.querySelector('.md-slider-container');
thumbLabelTextElement = sliderNativeElement.querySelector('.md-slider-thumb-label-text');
});
it('should add the thumb label class to the slider container', () => {
- expect(sliderContainerElement.classList).toContain('md-slider-thumb-label-showing');
+ expect(sliderNativeElement.classList).toContain('md-slider-thumb-label-showing');
});
it('should update the thumb label text on click', () => {
expect(thumbLabelTextElement.textContent).toBe('0');
- dispatchClickEvent(sliderNativeElement, 0.13);
+ dispatchClickEventSequence(sliderNativeElement, 0.13);
fixture.detectChanges();
// The thumb label text is set to the slider's value. These should always be the same.
@@ -577,26 +503,26 @@ describe('MdSlider', () => {
});
it('should show the thumb label on click', () => {
- expect(sliderContainerElement.classList).not.toContain('md-slider-active');
- expect(sliderContainerElement.classList).toContain('md-slider-thumb-label-showing');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-active');
+ expect(sliderNativeElement.classList).toContain('md-slider-thumb-label-showing');
- dispatchClickEvent(sliderNativeElement, 0.49);
+ dispatchClickEventSequence(sliderNativeElement, 0.49);
fixture.detectChanges();
// The thumb label appears when the slider is active and the 'md-slider-thumb-label-showing'
// class is applied.
- expect(sliderContainerElement.classList).toContain('md-slider-thumb-label-showing');
- expect(sliderContainerElement.classList).toContain('md-slider-active');
+ expect(sliderNativeElement.classList).toContain('md-slider-thumb-label-showing');
+ expect(sliderNativeElement.classList).toContain('md-slider-active');
});
it('should show the thumb label on slide', () => {
- expect(sliderContainerElement.classList).not.toContain('md-slider-active');
+ expect(sliderNativeElement.classList).not.toContain('md-slider-active');
dispatchSlideEventSequence(sliderNativeElement, 0, 0.91, gestureConfig);
fixture.detectChanges();
- expect(sliderContainerElement.classList).toContain('md-slider-thumb-label-showing');
- expect(sliderContainerElement.classList).toContain('md-slider-active');
+ expect(sliderNativeElement.classList).toContain('md-slider-thumb-label-showing');
+ expect(sliderNativeElement.classList).toContain('md-slider-active');
});
});
@@ -620,19 +546,19 @@ describe('MdSlider', () => {
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
});
- it('should update the control when the value is updated', () => {
+ it('should not update the control when the value is updated', () => {
expect(testComponent.control.value).toBe(0);
sliderInstance.value = 11;
fixture.detectChanges();
- expect(testComponent.control.value).toBe(11);
+ expect(testComponent.control.value).toBe(0);
});
it('should update the control on click', () => {
expect(testComponent.control.value).toBe(0);
- dispatchClickEvent(sliderNativeElement, 0.76);
+ dispatchClickEventSequence(sliderNativeElement, 0.76);
fixture.detectChanges();
expect(testComponent.control.value).toBe(76);
@@ -685,8 +611,6 @@ describe('MdSlider', () => {
let sliderTrackElement: HTMLElement;
let testComponent: SliderWithOneWayBinding;
let trackFillElement: HTMLElement;
- let thumbElement: HTMLElement;
- let sliderDimensions: ClientRect;
beforeEach(() => {
fixture = TestBed.createComponent(SliderWithOneWayBinding);
@@ -699,29 +623,19 @@ describe('MdSlider', () => {
sliderInstance = sliderDebugElement.injector.get(MdSlider);
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
trackFillElement = sliderNativeElement.querySelector('.md-slider-track-fill');
- thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
- sliderDimensions = sliderTrackElement.getBoundingClientRect();
});
it('should initialize based on bound value', () => {
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let thumbDimensions = thumbElement.getBoundingClientRect();
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
-
expect(sliderInstance.value).toBe(50);
- expect(thumbPosition).toBe(sliderDimensions.width / 2);
+ expect(trackFillElement.style.flexBasis).toBe('50%');
});
it('should update when bound value changes', () => {
testComponent.val = 75;
fixture.detectChanges();
- let trackFillDimensions = trackFillElement.getBoundingClientRect();
- let thumbDimensions = thumbElement.getBoundingClientRect();
- let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
-
expect(sliderInstance.value).toBe(75);
- expect(thumbPosition).toBe(sliderDimensions.width * 3 / 4);
+ expect(trackFillElement.style.flexBasis).toBe('75%');
});
});
@@ -731,24 +645,17 @@ describe('MdSlider', () => {
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
- let sliderDimensions: ClientRect;
- let thumbElement: HTMLElement;
- let thumbDimensions: ClientRect;
+ let trackFillElement: HTMLElement;
beforeEach(() => {
-
fixture = TestBed.createComponent(SliderWithValueSmallerThanMin);
fixture.detectChanges();
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.componentInstance;
-
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
- sliderDimensions = sliderTrackElement.getBoundingClientRect();
-
- thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
- thumbDimensions = thumbElement.getBoundingClientRect();
+ trackFillElement = sliderNativeElement.querySelector('.md-slider-track-fill');
});
it('should set the value smaller than the min value', () => {
@@ -757,9 +664,8 @@ describe('MdSlider', () => {
expect(sliderInstance.max).toBe(6);
});
- it('should place the thumb on the min value', () => {
- thumbDimensions = thumbElement.getBoundingClientRect();
- expect(thumbDimensions.left).toBe(sliderDimensions.left);
+ it('should set the fill to the min value', () => {
+ expect(trackFillElement.style.flexBasis).toBe('0%');
});
});
@@ -769,9 +675,7 @@ describe('MdSlider', () => {
let sliderNativeElement: HTMLElement;
let sliderInstance: MdSlider;
let sliderTrackElement: HTMLElement;
- let sliderDimensions: ClientRect;
- let thumbElement: HTMLElement;
- let thumbDimensions: ClientRect;
+ let trackFillElement: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SliderWithValueGreaterThanMax);
@@ -780,13 +684,8 @@ describe('MdSlider', () => {
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.componentInstance;
-
sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
- sliderDimensions = sliderTrackElement.getBoundingClientRect();
-
- thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
- thumbDimensions = thumbElement.getBoundingClientRect();
-
+ trackFillElement = sliderNativeElement.querySelector('.md-slider-track-fill');
});
it('should set the value greater than the max value', () => {
@@ -795,9 +694,8 @@ describe('MdSlider', () => {
expect(sliderInstance.max).toBe(6);
});
- it('should place the thumb on the max value', () => {
- thumbDimensions = thumbElement.getBoundingClientRect();
- expect(thumbDimensions.left).toBe(sliderDimensions.right);
+ it('should set the fill to the max value', () => {
+ expect(trackFillElement.style.flexBasis).toBe('100%');
});
});
@@ -823,7 +721,7 @@ describe('MdSlider', () => {
it('should emit change on click', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
- dispatchClickEvent(sliderNativeElement, 0.2);
+ dispatchClickEventSequence(sliderNativeElement, 0.2);
fixture.detectChanges();
expect(testComponent.onChange).toHaveBeenCalledTimes(1);
@@ -841,7 +739,7 @@ describe('MdSlider', () => {
it('should not emit multiple changes for same value', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
- dispatchClickEvent(sliderNativeElement, 0.6);
+ dispatchClickEventSequence(sliderNativeElement, 0.6);
dispatchSlideEventSequence(sliderNativeElement, 0.6, 0.6, gestureConfig);
fixture.detectChanges();
@@ -850,26 +748,28 @@ describe('MdSlider', () => {
});
});
-// The transition has to be removed in order to test the updated positions without setTimeout.
-const noTransitionStyle =
- '.md-slider-track-fill, .md-slider-thumb-position { transition: none !important; }';
+// Disable animations and make the slider an even 100px (+ 8px padding on either side)
+// so we get nice round values in tests.
+const styles = `
+ md-slider { min-width: 116px !important; }
+ .md-slider-track-fill { transition: none !important; }
+`;
@Component({
template: ``,
- styles: [noTransitionStyle],
- encapsulation: ViewEncapsulation.None
+ styles: [styles],
})
class StandardSlider { }
@Component({
- template: ``
+ template: ``,
+ styles: [styles],
})
class DisabledSlider { }
@Component({
template: ``,
- styles: [noTransitionStyle],
- encapsulation: ViewEncapsulation.None
+ styles: [styles],
})
class SliderWithMinAndMax {
min = 4;
@@ -877,60 +777,66 @@ class SliderWithMinAndMax {
}
@Component({
- template: ``
+ template: ``,
+ styles: [styles],
})
class SliderWithValue { }
@Component({
template: ``,
- styles: [noTransitionStyle],
- encapsulation: ViewEncapsulation.None
+ styles: [styles],
})
class SliderWithStep { }
-@Component({template: ``})
+@Component({
+ template: ``,
+ styles: [styles],
+})
class SliderWithAutoTickInterval { }
-@Component({template: ``})
+@Component({
+ template: ``,
+ styles: [styles],
+})
class SliderWithSetTickInterval { }
@Component({
template: ``,
- styles: [noTransitionStyle],
- encapsulation: ViewEncapsulation.None
+ styles: [styles],
})
class SliderWithThumbLabel { }
@Component({
- template: ``
+ template: ``,
+ styles: [styles],
})
class SliderWithOneWayBinding {
val = 50;
}
@Component({
- template: ``
+ template: ``,
+ styles: [styles],
})
class SliderWithTwoWayBinding {
- control = new FormControl('');
+ control = new FormControl(0);
}
@Component({
template: ``,
- styles: [noTransitionStyle],
- encapsulation: ViewEncapsulation.None
+ styles: [styles],
})
class SliderWithValueSmallerThanMin { }
@Component({
template: ``,
- styles: [noTransitionStyle],
- encapsulation: ViewEncapsulation.None
+ styles: [styles],
})
class SliderWithValueGreaterThanMax { }
@Component({
- template: ``
+ template: ``,
+ styles: [styles],
})
class SliderWithChangeHandler {
onChange() { }
@@ -943,12 +849,14 @@ class SliderWithChangeHandler {
* @param percentage The percentage of the slider where the click should occur. Used to find the
* physical location of the click.
*/
-function dispatchClickEvent(sliderElement: HTMLElement, percentage: number): void {
+function dispatchClickEventSequence(sliderElement: HTMLElement, percentage: number): void {
let trackElement = sliderElement.querySelector('.md-slider-track');
let dimensions = trackElement.getBoundingClientRect();
let y = dimensions.top;
let x = dimensions.left + (dimensions.width * percentage);
+ dispatchMouseenterEvent(sliderElement);
+
let event = document.createEvent('MouseEvent');
event.initMouseEvent(
'click', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null);
@@ -964,6 +872,7 @@ function dispatchClickEvent(sliderElement: HTMLElement, percentage: number): voi
*/
function dispatchSlideEventSequence(sliderElement: HTMLElement, startPercent: number,
endPercent: number, gestureConfig: TestGestureConfig): void {
+ dispatchMouseenterEvent(sliderElement);
dispatchSlideStartEvent(sliderElement, startPercent, gestureConfig);
dispatchSlideEvent(sliderElement, startPercent, gestureConfig);
dispatchSlideEvent(sliderElement, endPercent, gestureConfig);
@@ -1023,3 +932,22 @@ function dispatchSlideEndEvent(sliderElement: HTMLElement, percent: number,
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
});
}
+
+/**
+ * Dispatches a mouseenter event from an element.
+ * Note: The mouse event truncates the position for the click.
+ * @param trackElement The track element from which the event location will be calculated.
+ * @param containerElement The container element from which the event will be dispatched.
+ * @param percentage The percentage of the slider where the click should occur. Used to find the
+ * physical location of the click.
+ */
+function dispatchMouseenterEvent(element: HTMLElement): void {
+ let dimensions = element.getBoundingClientRect();
+ let y = dimensions.top;
+ let x = dimensions.left;
+
+ let event = document.createEvent('MouseEvent');
+ event.initMouseEvent(
+ 'mouseenter', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null);
+ element.dispatchEvent(event);
+}
diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts
index c86bc0b5c654..ac7f0c6912bc 100644
--- a/src/lib/slider/slider.ts
+++ b/src/lib/slider/slider.ts
@@ -1,19 +1,17 @@
import {
- NgModule,
- ModuleWithProviders,
- Component,
- ElementRef,
- EventEmitter,
- HostBinding,
- Input,
- Output,
- ViewEncapsulation,
- AfterContentInit,
- forwardRef,
+ NgModule,
+ ModuleWithProviders,
+ Component,
+ ElementRef,
+ Input,
+ Output,
+ ViewEncapsulation,
+ forwardRef,
+ EventEmitter,
} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
-import {MdGestureConfig, applyCssTransform, coerceBooleanProperty} from '../core';
+import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core';
import {Input as HammerInput} from 'hammerjs';
/**
@@ -43,30 +41,38 @@ export class MdSliderChange {
selector: 'md-slider',
providers: [MD_SLIDER_VALUE_ACCESSOR],
host: {
+ '(blur)': '_onBlur()',
+ '(click)': '_onClick($event)',
+ '(mouseenter)': '_onMouseenter()',
+ '(slide)': '_onSlide($event)',
+ '(slideend)': '_onSlideEnd()',
+ '(slidestart)': '_onSlideStart($event)',
'tabindex': '0',
- '(click)': 'onClick($event)',
- '(slide)': 'onSlide($event)',
- '(slidestart)': 'onSlideStart($event)',
- '(slideend)': 'onSlideEnd()',
- '(window:resize)': 'onResize()',
- '(blur)': 'onBlur()',
+ '[attr.aria-disabled]': 'disabled',
+ '[attr.aria-valuemax]': 'max',
+ '[attr.aria-valuemin]': 'min',
+ '[attr.aria-valuenow]': 'value',
+ '[class.md-slider-active]': '_isActive',
+ '[class.md-slider-disabled]': 'disabled',
+ '[class.md-slider-has-ticks]': 'tickInterval',
+ '[class.md-slider-sliding]': '_isSliding',
+ '[class.md-slider-thumb-label-showing]': 'thumbLabel',
},
templateUrl: 'slider.html',
styleUrls: ['slider.css'],
encapsulation: ViewEncapsulation.None,
})
-export class MdSlider implements AfterContentInit, ControlValueAccessor {
+export class MdSlider implements ControlValueAccessor {
/** A renderer to handle updating the slider's thumb and fill track. */
private _renderer: SliderRenderer = null;
/** The dimensions of the slider. */
private _sliderDimensions: ClientRect = null;
+ /** Whether or not the slider is disabled. */
private _disabled: boolean = false;
@Input()
- @HostBinding('class.md-slider-disabled')
- @HostBinding('attr.aria-disabled')
get disabled(): boolean { return this._disabled; }
set disabled(value) { this._disabled = coerceBooleanProperty(value); }
@@ -77,16 +83,7 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
get thumbLabel(): boolean { return this._thumbLabel; }
set thumbLabel(value) { this._thumbLabel = coerceBooleanProperty(value); }
- /** The miniumum value that the slider can have. */
- private _min: number = 0;
-
- /** The maximum value that the slider can have. */
- private _max: number = 100;
-
- /** The percentage of the slider that coincides with the value. */
- private _percent: number = 0;
-
- private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
+ private _controlValueAccessorChangeFn: (value: any) => void = () => {};
/** The last value for which a change event was emitted. */
private _lastEmittedValue: number = null;
@@ -94,82 +91,106 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
onTouched: () => any = () => {};
- /** The values at which the thumb will snap. */
- @Input() step: number = 1;
-
- /**
- * How often to show ticks. Relative to the step so that a tick always appears on a step.
- * Ex: Tick interval of 4 with a step of 3 will draw a tick every 4 steps (every 12 values).
- */
- @Input('tick-interval') _tickInterval: 'auto' | number;
-
/**
* Whether or not the thumb is sliding.
* Used to determine if there should be a transition for the thumb and fill track.
- * TODO: internal
*/
- isSliding: boolean = false;
+ _isSliding: boolean = false;
/**
* Whether or not the slider is active (clicked or sliding).
* Used to shrink and grow the thumb as according to the Material Design spec.
- * TODO: internal
*/
- isActive: boolean = false;
+ _isActive: boolean = false;
+
+ /** The values at which the thumb will snap. */
+ private _step: number = 1;
+
+ @Input()
+ get step() { return this._step; }
+ set step(v) { this._step = coerceNumberProperty(v, this._step); }
+
+ /**
+ * How often to show ticks. Relative to the step so that a tick always appears on a step.
+ * Ex: Tick interval of 4 with a step of 3 will draw a tick every 4 steps (every 12 values).
+ */
+ private _tickInterval: 'auto' | number = 0;
+
+ @Input('tick-interval')
+ get tickInterval() { return this._tickInterval; }
+ set tickInterval(v) {
+ this._tickInterval = (v == 'auto') ? v : coerceNumberProperty(v, this._tickInterval);
+ }
+
+ /** The size of a tick interval as a percentage of the size of the track. */
+ private _tickIntervalPercent: number = 0;
- /** Indicator for if the value has been set or not. */
- private _isInitialized: boolean = false;
+ get tickIntervalPercent() { return this._tickIntervalPercent; }
+
+ /** The percentage of the slider that coincides with the value. */
+ private _percent: number = 0;
+
+ get percent() { return this._clamp(this._percent); }
/** Value of the slider. */
- private _value: number = 0;
+ private _value: number = null;
+
+ @Input()
+ get value() {
+ // If the value needs to be read and it is still uninitialized, initialize it to the min.
+ if (this._value === null) {
+ this.value = this._min;
+ }
+ return this._value;
+ }
+ set value(v: number) {
+ this._value = coerceNumberProperty(v, this._value);
+ this._percent = this._calculatePercentage(this._value);
+ }
+
+ /** The miniumum value that the slider can have. */
+ private _min: number = 0;
@Input()
- @HostBinding('attr.aria-valuemin')
get min() {
return this._min;
}
-
set min(v: number) {
- // This has to be forced as a number to handle the math later.
- this._min = Number(v);
+ this._min = coerceNumberProperty(v, this._min);
// If the value wasn't explicitly set by the user, set it to the min.
- if (!this._isInitialized) {
+ if (this._value === null) {
this.value = this._min;
}
- this.snapThumbToValue();
- this._updateTickSeparation();
+ this._percent = this._calculatePercentage(this.value);
}
+ /** The maximum value that the slider can have. */
+ private _max: number = 100;
+
@Input()
- @HostBinding('attr.aria-valuemax')
get max() {
return this._max;
}
-
set max(v: number) {
- this._max = Number(v);
- this.snapThumbToValue();
- this._updateTickSeparation();
+ this._max = coerceNumberProperty(v, this._max);
+ this._percent = this._calculatePercentage(this.value);
}
- @Input()
- @HostBinding('attr.aria-valuenow')
- get value() {
- return this._value;
+ get trackFillFlexBasis() {
+ return this.percent * 100 + '%';
}
- set value(v: number) {
- // Only set the value to a valid number. v is casted to an any as we know it will come in as a
- // string but it is labeled as a number which causes parseFloat to not accept it.
- if (isNaN(parseFloat( v))) {
- return;
- }
+ get ticksMarginLeft() {
+ return this.tickIntervalPercent / 2 * 100 + '%';
+ }
- this._value = Number(v);
- this._isInitialized = true;
- this._controlValueAccessorChangeFn(this._value);
- this.snapThumbToValue();
+ get ticksContainerMarginLeft() {
+ return '-' + this.ticksMarginLeft;
+ }
+
+ get ticksBackgroundSize() {
+ return this.tickIntervalPercent * 100 + '% 2px';
}
@Output() change = new EventEmitter();
@@ -178,117 +199,81 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
this._renderer = new SliderRenderer(elementRef);
}
- /**
- * Once the slider has rendered, grab the dimensions and update the position of the thumb and
- * fill track.
- * TODO: internal
- */
- ngAfterContentInit() {
+ _onMouseenter() {
+ if (this.disabled) {
+ return;
+ }
+
+ // We save the dimensions of the slider here so we can use them to update the spacing of the
+ // ticks and determine where on the slider click and slide events happen.
this._sliderDimensions = this._renderer.getSliderDimensions();
- // This needs to be called after content init because the value can be set to the min if the
- // value itself isn't set. If this happens, the control value accessor needs to be updated.
- this._controlValueAccessorChangeFn(this.value);
- this.snapThumbToValue();
- this._updateTickSeparation();
+ this._updateTickIntervalPercent();
}
- /** TODO: internal */
- onClick(event: MouseEvent) {
+ _onClick(event: MouseEvent) {
if (this.disabled) {
return;
}
- this.isActive = true;
- this.isSliding = false;
+ this._isActive = true;
+ this._isSliding = false;
this._renderer.addFocus();
- this.updateValueFromPosition(event.clientX);
- this.snapThumbToValue();
+ this._updateValueFromPosition(event.clientX);
this._emitValueIfChanged();
}
- /** TODO: internal */
- onSlide(event: HammerInput) {
+ _onSlide(event: HammerInput) {
if (this.disabled) {
return;
}
// Prevent the slide from selecting anything else.
event.preventDefault();
- this.updateValueFromPosition(event.center.x);
+ this._updateValueFromPosition(event.center.x);
}
- /** TODO: internal */
- onSlideStart(event: HammerInput) {
+ _onSlideStart(event: HammerInput) {
if (this.disabled) {
return;
}
event.preventDefault();
- this.isSliding = true;
- this.isActive = true;
+ this._isSliding = true;
+ this._isActive = true;
this._renderer.addFocus();
- this.updateValueFromPosition(event.center.x);
+ this._updateValueFromPosition(event.center.x);
}
- /** TODO: internal */
- onSlideEnd() {
- this.isSliding = false;
- this.snapThumbToValue();
+ _onSlideEnd() {
+ this._isSliding = false;
this._emitValueIfChanged();
}
- /** TODO: internal */
- onResize() {
- this.isSliding = true;
- this._sliderDimensions = this._renderer.getSliderDimensions();
- // Skip updating the value and position as there is no new placement.
- this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
- }
-
- /** TODO: internal */
- onBlur() {
- this.isActive = false;
+ _onBlur() {
+ this._isActive = false;
this.onTouched();
}
- /**
- * When the value changes without a physical position, the percentage needs to be recalculated
- * independent of the physical location.
- * This is also used to move the thumb to a snapped value once sliding is done.
- */
- updatePercentFromValue() {
- this._percent = this.calculatePercentage(this.value);
- }
-
/**
* Calculate the new value from the new physical location. The value will always be snapped.
*/
- updateValueFromPosition(pos: number) {
+ private _updateValueFromPosition(pos: number) {
+ if (!this._sliderDimensions) {
+ return;
+ }
+
let offset = this._sliderDimensions.left;
let size = this._sliderDimensions.width;
// The exact value is calculated from the event and used to find the closest snap value.
- this._percent = this.clamp((pos - offset) / size);
- let exactValue = this.calculateValue(this._percent);
+ let percent = this._clamp((pos - offset) / size);
+ let exactValue = this._calculateValue(percent);
// This calculation finds the closest step by finding the closest whole number divisible by the
// step relative to the min.
let closestValue = Math.round((exactValue - this.min) / this.step) * this.step + this.min;
// The value needs to snap to the min and max.
- this.value = this.clamp(closestValue, this.min, this.max);
- this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
- }
-
- /**
- * Snaps the thumb to the current value.
- * Called after a click or drag event is over.
- */
- snapThumbToValue() {
- this.updatePercentFromValue();
- if (this._sliderDimensions) {
- let renderedPercent = this.clamp(this._percent);
- this._renderer.updateThumbAndFillPosition(renderedPercent, this._sliderDimensions.width);
- }
+ this.value = this._clamp(closestValue, this.min, this.max);
}
/** Emits a change event if the current value is different from the last emitted value. */
@@ -298,97 +283,52 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
event.source = this;
event.value = this.value;
this.change.emit(event);
+ this._controlValueAccessorChangeFn(this.value);
this._lastEmittedValue = this.value;
}
}
/**
- * Calculates the separation in pixels of tick marks. If there is no tick interval or the interval
- * is set to something other than a number or 'auto', nothing happens.
+ * Updates the amount of space between ticks as a percentage of the width of the slider.
*/
- private _updateTickSeparation() {
- if (!this._sliderDimensions) {
+ private _updateTickIntervalPercent() {
+ if (!this.tickInterval) {
return;
}
- if (this._tickInterval == 'auto') {
- this._updateAutoTickSeparation();
- } else if (Number(this._tickInterval)) {
- this._updateTickSeparationFromInterval();
- }
- }
-
- /**
- * Calculates the optimal separation in pixels of tick marks based on the minimum auto tick
- * separation constant.
- */
- private _updateAutoTickSeparation() {
- // We're looking for the multiple of step for which the separation between is greater than the
- // minimum tick separation.
- let sliderWidth = this._sliderDimensions.width;
-
- // This is the total "width" of the slider in terms of values.
- let valueWidth = this.max - this.min;
-
- // Calculate how many values exist within 1px on the slider.
- let valuePerPixel = valueWidth / sliderWidth;
-
- // Calculate how many values exist in the minimum tick separation (px).
- let valuePerSeparation = valuePerPixel * MIN_AUTO_TICK_SEPARATION;
-
- // Calculate how many steps exist in this separation. This will be the lowest value you can
- // multiply step by to get a separation that is greater than or equal to the minimum tick
- // separation.
- let stepsPerSeparation = Math.ceil(valuePerSeparation / this.step);
-
- // Get the percentage of the slider for which this tick would be located so we can then draw
- // it on the slider.
- let tickPercentage = this.calculatePercentage((this.step * stepsPerSeparation) + this.min);
- // The pixel value of the tick is the percentage * the width of the slider. Use this to draw
- // the ticks on the slider.
- this._renderer.drawTicks(sliderWidth * tickPercentage);
- }
-
- /**
- * Calculates the separation of tick marks by finding the pixel value of the tickInterval.
- */
- private _updateTickSeparationFromInterval() {
- // Force tickInterval to be a number so it can be used in calculations.
- let interval: number = this._tickInterval;
- // Calculate the first value a tick will be located at by getting the step at which the interval
- // lands and adding that to the min.
- let tickValue = (this.step * interval) + this.min;
-
- // The percentage of the step on the slider is needed in order to calculate the pixel offset
- // from the beginning of the slider. This offset is the tick separation.
- let tickPercentage = this.calculatePercentage(tickValue);
- this._renderer.drawTicks(this._sliderDimensions.width * tickPercentage);
+ if (this.tickInterval == 'auto') {
+ let pixelsPerStep = this._sliderDimensions.width * this.step / (this.max - this.min);
+ let stepsPerTick = Math.ceil(MIN_AUTO_TICK_SEPARATION / pixelsPerStep);
+ let pixelsPerTick = stepsPerTick * this.step;
+ this._tickIntervalPercent = pixelsPerTick / (this._sliderDimensions.width);
+ } else {
+ this._tickIntervalPercent = this.tickInterval * this.step / (this.max - this.min);
+ }
}
/**
* Calculates the percentage of the slider that a value is.
*/
- calculatePercentage(value: number) {
+ private _calculatePercentage(value: number) {
return (value - this.min) / (this.max - this.min);
}
/**
* Calculates the value a percentage of the slider corresponds to.
*/
- calculateValue(percentage: number) {
- return this.min + (percentage * (this.max - this.min));
+ private _calculateValue(percentage: number) {
+ return this.min + percentage * (this.max - this.min);
}
/**
* Return a number between two numbers.
*/
- clamp(value: number, min = 0, max = 1) {
+ private _clamp(value: number, min = 0, max = 1) {
return Math.max(min, Math.min(value, max));
}
/**
* Implemented as part of ControlValueAccessor.
- * TODO: internal
*/
writeValue(value: any) {
this.value = value;
@@ -396,7 +336,6 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
/**
* Implemented as part of ControlValueAccessor.
- * TODO: internal
*/
registerOnChange(fn: (value: any) => void) {
this._controlValueAccessorChangeFn = fn;
@@ -404,15 +343,14 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
/**
* Implemented as part of ControlValueAccessor.
- * TODO: internal
*/
registerOnTouched(fn: any) {
this.onTouched = fn;
}
/**
- * Implemented as part of ControlValueAccessor
- */
+ * Implemented as part of ControlValueAccessor.
+ */
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
@@ -438,21 +376,6 @@ export class SliderRenderer {
return trackElement.getBoundingClientRect();
}
- /**
- * Update the physical position of the thumb and fill track on the slider.
- */
- updateThumbAndFillPosition(percent: number, width: number) {
- // A container element that is used to avoid overwriting the transform on the thumb itself.
- let thumbPositionElement =
- this._sliderElement.querySelector('.md-slider-thumb-position');
- let fillTrackElement = this._sliderElement.querySelector('.md-slider-track-fill');
-
- let position = Math.round(percent * width);
-
- fillTrackElement.style.width = `${position}px`;
- applyCssTransform(thumbPositionElement, `translateX(${position}px)`);
- }
-
/**
* Focuses the native element.
* Currently only used to allow a blur event to fire but will be used with keyboard input later.
@@ -460,38 +383,6 @@ export class SliderRenderer {
addFocus() {
this._sliderElement.focus();
}
-
- /**
- * Draws ticks onto the tick container.
- */
- drawTicks(tickSeparation: number) {
- let sliderTrackContainer =
- this._sliderElement.querySelector('.md-slider-track-container');
- let tickContainerWidth = sliderTrackContainer.getBoundingClientRect().width;
- let tickContainer = this._sliderElement.querySelector('.md-slider-tick-container');
- // An extra element for the last tick is needed because the linear gradient cannot be told to
- // always draw a tick at the end of the gradient. To get around this, there is a second
- // container for ticks that has a single tick mark on the very right edge.
- let lastTickContainer =
- this._sliderElement.querySelector('.md-slider-last-tick-container');
- // Subtract 1 from the tick separation to center the tick.
- // TODO: Evaluate the rendering performance of using repeating background gradients.
- tickContainer.style.background = `repeating-linear-gradient(to right, black, black 2px, ` +
- `transparent 2px, transparent ${tickSeparation - 1}px)`;
- // Add a tick to the very end by starting on the right side and adding a 2px black line.
- lastTickContainer.style.background = `linear-gradient(to left, black, black 2px, transparent ` +
- `2px, transparent)`;
-
- if (tickContainerWidth % tickSeparation < (tickSeparation / 2)) {
- // If the second to last tick is too close (a separation of less than half the normal
- // separation), don't show it by decreasing the width of the tick container element.
- tickContainer.style.width = tickContainerWidth - tickSeparation + 'px';
- } else {
- // If there is enough space for the second-to-last tick, restore the default width of the
- // tick container.
- tickContainer.style.width = '';
- }
- }
}
diff --git a/stylelint-config.json b/stylelint-config.json
index 28d5a47e51b1..17f3c7292822 100644
--- a/stylelint-config.json
+++ b/stylelint-config.json
@@ -28,7 +28,9 @@
"property-case": "lower",
- "declaration-block-no-duplicate-properties": true,
+ "declaration-block-no-duplicate-properties": [ true, {
+ "ignore": ["consecutive-duplicates-with-different-values"]
+ }],
"declaration-block-no-ignored-properties": true,
"declaration-block-trailing-semicolon": "always",
"declaration-block-single-line-max-declarations": 1,