From 6dd63d54de8c49b52f11b2f63f0c628772e30207 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 31 Oct 2016 15:54:15 -0700 Subject: [PATCH 01/11] fix(slider): refactor the slider to use percent values for the track fill and thumb position. --- src/demo-app/slider/slider-demo.html | 9 + src/lib/slider/_slider-theme.scss | 3 +- src/lib/slider/slider.html | 30 +- src/lib/slider/slider.scss | 180 ++++------ src/lib/slider/slider.spec.ts | 517 ++++++++++++--------------- src/lib/slider/slider.ts | 371 ++++++++----------- 6 files changed, 462 insertions(+), 648 deletions(-) diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html index 2050cd374534..e1724ed3a87a 100644 --- a/src/demo-app/slider/slider-demo.html +++ b/src/demo-app/slider/slider-demo.html @@ -33,3 +33,12 @@

Slider with one-way binding

Slider with two-way binding

+ + + + + + + \ No newline at end of file diff --git a/src/lib/slider/_slider-theme.scss b/src/lib/slider/_slider-theme.scss index 28f67927e7b3..d4a196c3daf2 100644 --- a/src/lib/slider/_slider-theme.scss +++ b/src/lib/slider/_slider-theme.scss @@ -18,9 +18,8 @@ background-color: md-color($accent); } - .md-slider-thumb::after { + .md-slider-thumb { background-color: md-color($accent); - border-color: md-color($accent); } .md-slider-thumb-label { diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html index 9d502deca0a2..97a04584dd4a 100644 --- a/src/lib/slider/slider.html +++ b/src/lib/slider/slider.html @@ -1,21 +1,13 @@ -
-
-
-
-
-
-
-
-
-
-
-
- {{value}} -
-
+
+
+
+
+
+
+
+
+ {{value}}
-
+
\ No newline at end of file diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index 04243e0775df..d743d9ad189c 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -7,148 +7,129 @@ $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, .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%; + 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-x; + 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 +139,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..5d0193222602 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,84 @@ 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(sliderTrackElement, 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. + + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.89, gestureConfig); 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); + + dispatchSlideEventSequence(sliderTrackElement, 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); + + dispatchSlideEventSequence(sliderTrackElement, 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); + expect(trackFillElement.style.flexBasis).toBe('0%'); - trackFillDimensions = trackFillElement.getBoundingClientRect(); - thumbDimensions = thumbElement.getBoundingClientRect(); - - // 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(sliderTrackElement, 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); + expect(trackFillElement.style.flexBasis).toBe('0%'); - trackFillDimensions = trackFillElement.getBoundingClientRect(); - thumbDimensions = thumbElement.getBoundingClientRect(); - - // 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(sliderTrackElement, 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(sliderTrackElement, 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(sliderTrackElement, 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(); 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); + dispatchSlideStartEvent(sliderTrackElement, sliderNativeElement, 0, gestureConfig); fixture.detectChanges(); - expect(containerElement.classList).toContain('md-slider-sliding'); + expect(sliderNativeElement.classList).toContain('md-slider-sliding'); - dispatchSlideEndEvent(sliderNativeElement, 0.34, gestureConfig); + dispatchSlideEndEvent(sliderTrackElement, sliderNativeElement, 0.34, gestureConfig); fixture.detectChanges(); - expect(containerElement.classList).not.toContain('md-slider-sliding'); + expect(sliderNativeElement.classList).not.toContain('md-slider-sliding'); }); }); @@ -214,34 +171,36 @@ describe('MdSlider', () => { it('should not change the value on click when disabled', () => { expect(sliderInstance.value).toBe(0); - dispatchClickEvent(sliderNativeElement, 0.63); + + dispatchClickEventSequence(sliderTrackElement, 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); + + dispatchSlideEventSequence(sliderNativeElement, 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(sliderTrackElement, 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); + dispatchSlideStartEvent(sliderTrackElement, sliderNativeElement, 0.46, gestureConfig); fixture.detectChanges(); - expect(containerElement.classList).not.toContain('md-slider-sliding'); + expect(sliderNativeElement.classList).not.toContain('md-slider-sliding'); }); }); @@ -251,10 +210,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 +224,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 +237,9 @@ describe('MdSlider', () => { }); it('should set the correct value on click', () => { - dispatchClickEvent(sliderNativeElement, 0.09); + dispatchClickEventSequence(sliderTrackElement, 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))); @@ -288,70 +247,60 @@ describe('MdSlider', () => { }); it('should set the correct value on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.62, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, 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(sliderTrackElement, 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', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.74, gestureConfig); + it('should snap the fill to the nearest value on slide', () => { + dispatchSlideEventSequence(sliderTrackElement, 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,14 +326,18 @@ describe('MdSlider', () => { }); it('should set the correct value on click', () => { - dispatchClickEvent(sliderNativeElement, 0.92); + dispatchClickEventSequence(sliderTrackElement, 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); }); it('should set the correct value on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.32, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig); + fixture.detectChanges(); + expect(sliderInstance.value).toBe(32); }); }); @@ -395,9 +348,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,51 +358,39 @@ 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(sliderTrackElement, 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(sliderTrackElement, 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', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.07, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.07, gestureConfig); fixture.detectChanges(); expect(sliderInstance.value).toBe(0); }); it('should snap the thumb and fill to a step on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.88, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, 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 +398,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 +407,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'); - }); - - 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)'); + ticksContainerElement = + sliderNativeElement.querySelector('.md-slider-ticks-container'); + ticksElement = sliderNativeElement.querySelector('.md-slider-ticks'); }); - 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 +428,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 +437,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 +461,6 @@ describe('MdSlider', () => { let sliderNativeElement: HTMLElement; let sliderInstance: MdSlider; let sliderTrackElement: HTMLElement; - let sliderContainerElement: Element; let thumbLabelTextElement: Element; beforeEach(() => { @@ -548,18 +471,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(sliderTrackElement, sliderNativeElement, 0.13); fixture.detectChanges(); // The thumb label text is set to the slider's value. These should always be the same. @@ -569,7 +491,7 @@ describe('MdSlider', () => { it('should update the thumb label text on slide', () => { expect(thumbLabelTextElement.textContent).toBe('0'); - dispatchSlideEventSequence(sliderNativeElement, 0, 0.56, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.56, gestureConfig); fixture.detectChanges(); // The thumb label text is set to the slider's value. These should always be the same. @@ -577,26 +499,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(sliderTrackElement, 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); + dispatchSlideEventSequence(sliderTrackElement, 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'); }); }); @@ -632,7 +554,7 @@ describe('MdSlider', () => { it('should update the control on click', () => { expect(testComponent.control.value).toBe(0); - dispatchClickEvent(sliderNativeElement, 0.76); + dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.76); fixture.detectChanges(); expect(testComponent.control.value).toBe(76); @@ -641,7 +563,7 @@ describe('MdSlider', () => { it('should update the control on slide', () => { expect(testComponent.control.value).toBe(0); - dispatchSlideEventSequence(sliderNativeElement, 0, 0.19, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.19, gestureConfig); fixture.detectChanges(); expect(testComponent.control.value).toBe(19); @@ -685,8 +607,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 +619,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 +641,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 +660,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 +671,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 +680,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 +690,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 +717,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 +735,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 +744,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,39 +773,46 @@ 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(''); @@ -917,72 +820,76 @@ class SliderWithTwoWayBinding { @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() { } } /** - * Dispatches a click event from an element. + * Dispatches a click event sequence (consisting of moueseenter, click) from an element. * Note: The mouse event truncates the position for the click. - * @param sliderElement The md-slider element from which the event will be dispatched. + * @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 dispatchClickEvent(sliderElement: HTMLElement, percentage: number): void { - let trackElement = sliderElement.querySelector('.md-slider-track'); +function dispatchClickEventSequence(trackElement: HTMLElement, containerElement: HTMLElement, + percentage: number): void { let dimensions = trackElement.getBoundingClientRect(); let y = dimensions.top; let x = dimensions.left + (dimensions.width * percentage); + dispatchMouseenterEvent(containerElement); + let event = document.createEvent('MouseEvent'); event.initMouseEvent( 'click', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null); - sliderElement.dispatchEvent(event); + containerElement.dispatchEvent(event); } /** * Dispatches a slide event sequence (consisting of slidestart, slide, slideend) from an element. - * @param sliderElement The md-slider element from which the event will be dispatched. + * @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 startPercent The percentage of the slider where the slide will begin. * @param endPercent The percentage of the slider where the slide will end. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideEventSequence(sliderElement: HTMLElement, startPercent: number, - endPercent: number, gestureConfig: TestGestureConfig): void { - dispatchSlideStartEvent(sliderElement, startPercent, gestureConfig); - dispatchSlideEvent(sliderElement, startPercent, gestureConfig); - dispatchSlideEvent(sliderElement, endPercent, gestureConfig); - dispatchSlideEndEvent(sliderElement, endPercent, gestureConfig); +function dispatchSlideEventSequence(trackElement: HTMLElement, containerElement: HTMLElement, + startPercent: number, endPercent: number, + gestureConfig: TestGestureConfig): void { + dispatchSlideStartEvent(trackElement, containerElement, startPercent, gestureConfig); + dispatchSlideEvent(trackElement, containerElement, startPercent, gestureConfig); + dispatchSlideEvent(trackElement, containerElement, endPercent, gestureConfig); + dispatchSlideEndEvent(trackElement, containerElement, endPercent, gestureConfig); } /** * Dispatches a slide event from an element. - * @param sliderElement The md-slider element from which the event will be dispatched. + * @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 percent The percentage of the slider where the slide will happen. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideEvent(sliderElement: HTMLElement, percent: number, - gestureConfig: TestGestureConfig): void { - let trackElement = sliderElement.querySelector('.md-slider-track'); +function dispatchSlideEvent(trackElement: HTMLElement, containerElement: HTMLElement, + percent: number, gestureConfig: TestGestureConfig): void { let dimensions = trackElement.getBoundingClientRect(); let x = dimensions.left + (dimensions.width * percent); - gestureConfig.emitEventForElement('slide', sliderElement, { + gestureConfig.emitEventForElement('slide', containerElement, { center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); @@ -990,17 +897,19 @@ function dispatchSlideEvent(sliderElement: HTMLElement, percent: number, /** * Dispatches a slidestart event from an element. - * @param sliderElement The md-slider element from which the event will be dispatched. + * @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 percent The percentage of the slider where the slide will begin. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideStartEvent(sliderElement: HTMLElement, percent: number, - gestureConfig: TestGestureConfig): void { - let trackElement = sliderElement.querySelector('.md-slider-track'); +function dispatchSlideStartEvent(trackElement: HTMLElement, containerElement: HTMLElement, + percent: number, gestureConfig: TestGestureConfig): void { let dimensions = trackElement.getBoundingClientRect(); let x = dimensions.left + (dimensions.width * percent); - gestureConfig.emitEventForElement('slidestart', sliderElement, { + dispatchMouseenterEvent(containerElement); + + gestureConfig.emitEventForElement('slidestart', containerElement, { center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); @@ -1008,18 +917,38 @@ function dispatchSlideStartEvent(sliderElement: HTMLElement, percent: number, /** * Dispatches a slideend event from an element. - * @param sliderElement The md-slider element from which the event will be dispatched. + * @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 percent The percentage of the slider where the slide will end. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideEndEvent(sliderElement: HTMLElement, percent: number, - gestureConfig: TestGestureConfig): void { - let trackElement = sliderElement.querySelector('.md-slider-track'); +function dispatchSlideEndEvent(trackElement: HTMLElement, containerElement: HTMLElement, + percent: number, gestureConfig: TestGestureConfig): void { let dimensions = trackElement.getBoundingClientRect(); let x = dimensions.left + (dimensions.width * percent); - gestureConfig.emitEventForElement('slideend', sliderElement, { + gestureConfig.emitEventForElement('slideend', containerElement, { center: { x: x }, 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..67cd024a54bd 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -1,19 +1,16 @@ 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} from '../core'; import {Input as HammerInput} from 'hammerjs'; /** @@ -43,30 +40,42 @@ export class MdSliderChange { selector: 'md-slider', providers: [MD_SLIDER_VALUE_ACCESSOR], host: { - 'tabindex': '0', + '(blur)': 'onBlur()', '(click)': 'onClick($event)', + '(mouseenter)': 'onMouseenter()', '(slide)': 'onSlide($event)', - '(slidestart)': 'onSlideStart($event)', '(slideend)': 'onSlideEnd()', + '(slidestart)': 'onSlideStart($event)', '(window:resize)': 'onResize()', - '(blur)': 'onBlur()', + + 'tabindex': '0', + + '[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 +86,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,15 +94,6 @@ 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. @@ -117,48 +108,64 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { */ isActive: boolean = false; - /** Indicator for if the value has been set or not. */ - private _isInitialized: boolean = false; - - /** Value of the slider. */ - private _value: number = 0; + /** The values at which the thumb will snap. */ + private _step: number = 1; @Input() - @HostBinding('attr.aria-valuemin') - get min() { - return this._min; + get step() { + return this._step; } - - set min(v: number) { - // This has to be forced as a number to handle the math later. - this._min = Number(v); - - // If the value wasn't explicitly set by the user, set it to the min. - if (!this._isInitialized) { - this.value = this._min; + set step(v) { + // 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; } - this.snapThumbToValue(); - this._updateTickSeparation(); + this._step = Number(v); } - @Input() - @HostBinding('attr.aria-valuemax') - get max() { - return this._max; + /** + * 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: 'auto' | number) { + // Only set the tickInterval 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 parseInt to not accept it. + if (v == 'auto') { + this._tickInterval = v; + } else { + let intV = parseInt( v); + if (!isNaN(intV)) { + this._tickInterval = intV; + } + } } - set max(v: number) { - this._max = Number(v); - this.snapThumbToValue(); - this._updateTickSeparation(); - } + /** The size of a tick interval as a percentage of the size of the track. */ + private _tickIntervalPercent: number = 0; + + 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 = null; @Input() - @HostBinding('attr.aria-valuenow') 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) { // 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. @@ -167,9 +174,44 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { } this._value = Number(v); - this._isInitialized = true; + this._percent = this._calculatePercentage(this._value); this._controlValueAccessorChangeFn(this._value); - this.snapThumbToValue(); + } + + /** The miniumum value that the slider can have. */ + private _min: number = 0; + + @Input() + get min() { + return this._min; + } + set min(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; + } + + // This has to be forced as a number to handle the math later. + this._min = Number(v); + + // If the value wasn't explicitly set by the user, set it to the min. + if (this._value === null) { + this.value = this._min; + } + this._percent = this._calculatePercentage(this.value); + } + + /** The maximum value that the slider can have. */ + private _max: number = 100; + + @Input() + get max() { + return this._max; + } + set max(v: number) { + this._max = Number(v); + this._percent = this._calculatePercentage(this.value); } @Output() change = new EventEmitter(); @@ -178,18 +220,14 @@ 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() { + /** TODO: internal */ + onMouseenter() { + if (this.disabled) { + return; + } + 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 */ @@ -201,8 +239,7 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { this.isActive = true; this.isSliding = false; this._renderer.addFocus(); - this.updateValueFromPosition(event.clientX); - this.snapThumbToValue(); + this._updateValueFromPosition(event.clientX); this._emitValueIfChanged(); } @@ -214,7 +251,7 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { // Prevent the slide from selecting anything else. event.preventDefault(); - this.updateValueFromPosition(event.center.x); + this._updateValueFromPosition(event.center.x); } /** TODO: internal */ @@ -227,22 +264,22 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { 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(); this._emitValueIfChanged(); } /** TODO: internal */ onResize() { + // Consider the slider to be sliding during resize to prevent animation. 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); + setTimeout(() => { + this.isSliding = false; + }, 0); } /** TODO: internal */ @@ -251,44 +288,26 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { 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. */ @@ -303,86 +322,41 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { } /** - * 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)); } @@ -411,8 +385,8 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { } /** - * Implemented as part of ControlValueAccessor - */ + * Implemented as part of ControlValueAccessor. + */ setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } @@ -438,21 +412,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 +419,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 = ''; - } - } } From dccac45f79cf8d596e2b66209e580bbdf45c8bed Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 31 Oct 2016 16:21:01 -0700 Subject: [PATCH 02/11] Fix lint error. --- src/lib/slider/slider.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index d743d9ad189c..fd228dddaa84 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -17,7 +17,7 @@ $md-slider-thumb-arrow-gap: 12px !default; $md-slider-thumb-label-size: 28px !default; -$md-slider-tick-color: rgba(0, 0, 0, .6) !default; +$md-slider-tick-color: rgba(0, 0, 0, 0.6) !default; $md-slider-tick-size: 2px !default; From dda95a44a9a5bd6b8f118d91c3bdc3815cf1b16f Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 31 Oct 2016 16:47:53 -0700 Subject: [PATCH 03/11] Fix tick position on firefox. --- src/lib/slider/slider.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index fd228dddaa84..a4b17e6ff1dd 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -59,6 +59,8 @@ md-slider { .md-slider-ticks-container { position: absolute; + left: 0; + top: 0; height: $md-slider-track-thickness; width: 100%; overflow: hidden; From 9a038575efd4909c3a8d9ea29137729363ebd345 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 1 Nov 2016 09:52:54 -0700 Subject: [PATCH 04/11] Crisper ticks on firefox. --- src/lib/slider/slider.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index a4b17e6ff1dd..7c1c36781386 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -68,7 +68,11 @@ md-slider { .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-x; + $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; From 31eb479d54f71879c736153840056af9b4f22311 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 1 Nov 2016 10:39:11 -0700 Subject: [PATCH 05/11] Allow consecutive duplicate properties in SCSS. --- stylelint-config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stylelint-config.json b/stylelint-config.json index 28d5a47e51b1..c50d6a9f6d4b 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"] + }], "declaration-block-no-ignored-properties": true, "declaration-block-trailing-semicolon": "always", "declaration-block-single-line-max-declarations": 1, From 42ed436a0f45e1a3e3cfe665a9cdb8cb5918021d Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 3 Nov 2016 15:07:15 -0700 Subject: [PATCH 06/11] Addressed comments. --- package.json | 2 +- src/lib/core/coersion/number-property.spec.ts | 76 +++++++++++ src/lib/core/coersion/number-property.ts | 4 + src/lib/core/core.ts | 1 + src/lib/slider/slider.html | 8 +- src/lib/slider/slider.scss | 4 +- src/lib/slider/slider.spec.ts | 2 +- src/lib/slider/slider.ts | 124 ++++++------------ stylelint-config.json | 2 +- 9 files changed, 127 insertions(+), 96 deletions(-) create mode 100644 src/lib/core/coersion/number-property.spec.ts create mode 100644 src/lib/core/coersion/number-property.ts diff --git a/package.json b/package.json index 7fe21b6c32ec..3ff0352afe59 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "run-sequence": "^1.2.2", "sass": "^0.5.0", "strip-ansi": "^3.0.0", - "stylelint": "^6.9.0", + "stylelint": "^7.5.0", "symlink-or-copy": "^1.0.1", "ts-node": "^0.7.3", "tslint": "^3.13.0", diff --git a/src/lib/core/coersion/number-property.spec.ts b/src/lib/core/coersion/number-property.spec.ts new file mode 100644 index 000000000000..7d06f481de61 --- /dev/null +++ b/src/lib/core/coersion/number-property.spec.ts @@ -0,0 +1,76 @@ +import {coerceNumberProperty} from './number-property'; + + +describe('coerceNumberProperty', () => { + it('should coerce undefined to 0 or default', () => { + expect(coerceNumberProperty(undefined)).toBe(0); + expect(coerceNumberProperty(undefined, 111)).toBe(111); + }); + + it('should coerce null to 0 or default', () => { + expect(coerceNumberProperty(null)).toBe(0); + expect(coerceNumberProperty(null, 111)).toBe(111); + }); + + it('should coerce true to 0 or default', () => { + expect(coerceNumberProperty(true)).toBe(0); + expect(coerceNumberProperty(true, 111)).toBe(111); + }); + + it('should coerce false to 0 or default', () => { + expect(coerceNumberProperty(false)).toBe(0); + expect(coerceNumberProperty(false, 111)).toBe(111); + + }); + + it('should coerce the empty string to 0 or default', () => { + expect(coerceNumberProperty('')).toBe(0); + expect(coerceNumberProperty('', 111)).toBe(111); + + }); + + it('should coerce the string "1" to 1', () => { + expect(coerceNumberProperty('1')).toBe(1); + expect(coerceNumberProperty('1', 111)).toBe(1); + }); + + it('should coerce the string "123.456" to 123.456', () => { + expect(coerceNumberProperty('123.456')).toBe(123.456); + expect(coerceNumberProperty('123.456', 111)).toBe(123.456); + }); + + it('should coerce the string "-123.456" to -123.456', () => { + expect(coerceNumberProperty('-123.456')).toBe(-123.456); + expect(coerceNumberProperty('-123.456', 111)).toBe(-123.456); + }); + + it('should coerce an arbitrary string to 0 or default', () => { + expect(coerceNumberProperty('pink')).toBe(0); + expect(coerceNumberProperty('pink', 111)).toBe(111); + }); + + it('should coerce the number 1 to 1', () => { + expect(coerceNumberProperty(1)).toBe(1); + expect(coerceNumberProperty(1, 111)).toBe(1); + }); + + it('should coerce the number 123.456 to 123.456', () => { + expect(coerceNumberProperty(123.456)).toBe(123.456); + expect(coerceNumberProperty(123.456, 111)).toBe(123.456); + }); + + it('should coerce the number -123.456 to -123.456', () => { + expect(coerceNumberProperty(-123.456)).toBe(-123.456); + expect(coerceNumberProperty(-123.456, 111)).toBe(-123.456); + }); + + it('should coerce an object to 0 or default', () => { + expect(coerceNumberProperty({})).toBe(0); + expect(coerceNumberProperty({}, 111)).toBe(111); + }); + + it('should coerce an array to 0 or default', () => { + expect(coerceNumberProperty([])).toBe(0); + expect(coerceNumberProperty([], 111)).toBe(111); + }); +}); diff --git a/src/lib/core/coersion/number-property.ts b/src/lib/core/coersion/number-property.ts new file mode 100644 index 000000000000..708bea97270a --- /dev/null +++ b/src/lib/core/coersion/number-property.ts @@ -0,0 +1,4 @@ +/** Coerces a data-bound value (typically a string) to a number. */ +export function coerceNumberProperty(value: any, fallbackValue = 0) { + return isNaN(parseFloat(value as any)) ? fallbackValue : Number(value); +} diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index 5802e3b40284..6c6142cf1a08 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -83,6 +83,7 @@ export * from './animation/animation'; // Coersion export {coerceBooleanProperty} from './coersion/boolean-property'; +export {coerceNumberProperty} from './coersion/number-property'; @NgModule({ diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html index 97a04584dd4a..01b6acc917ed 100644 --- a/src/lib/slider/slider.html +++ b/src/lib/slider/slider.html @@ -1,8 +1,8 @@
-
-
-
+
+
+
diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index 7c1c36781386..a8ae53908b05 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -69,8 +69,8 @@ md-slider { .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). */ + // 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; diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts index 5d0193222602..42a2a4c4c201 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -127,7 +127,7 @@ describe('MdSlider', () => { 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(sliderNativeElement.classList).not.toContain('md-slider-active'); diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index 67cd024a54bd..a6bbfd5ab489 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -6,12 +6,14 @@ import { Input, Output, ViewEncapsulation, - forwardRef, EventEmitter, + forwardRef, + EventEmitter, } from '@angular/core'; import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms'; import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; -import {MdGestureConfig, coerceBooleanProperty} from '../core'; +import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core'; import {Input as HammerInput} from 'hammerjs'; +import {CommonModule} from '@angular/common'; /** * Visually, a 30px separation between tick marks looks best. This is very subjective but it is @@ -40,25 +42,21 @@ 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)', - '(window:resize)': 'onResize()', - + '(blur)': '_onBlur()', + '(click)': '_onClick($event)', + '(mouseenter)': '_onMouseenter()', + '(slide)': '_onSlide($event)', + '(slideend)': '_onSlideEnd()', + '(slidestart)': '_onSlideStart($event)', 'tabindex': '0', - '[attr.aria-disabled]': 'disabled', '[attr.aria-valuemax]': 'max', '[attr.aria-valuemin]': 'min', '[attr.aria-valuenow]': 'value', - - '[class.md-slider-active]': 'isActive', + '[class.md-slider-active]': '_isActive', '[class.md-slider-disabled]': 'disabled', '[class.md-slider-has-ticks]': 'tickInterval', - '[class.md-slider-sliding]': 'isSliding', + '[class.md-slider-sliding]': '_isSliding', '[class.md-slider-thumb-label-showing]': 'thumbLabel', }, templateUrl: 'slider.html', @@ -97,32 +95,21 @@ export class MdSlider implements ControlValueAccessor { /** * 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) { - // 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; - } - this._step = Number(v); - } + 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. @@ -132,23 +119,15 @@ export class MdSlider implements ControlValueAccessor { @Input('tick-interval') get tickInterval() { return this._tickInterval; } - set tickInterval(v: 'auto' | number) { - // Only set the tickInterval 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 parseInt to not accept it. - if (v == 'auto') { - this._tickInterval = v; - } else { - let intV = parseInt( v); - if (!isNaN(intV)) { - this._tickInterval = intV; - } - } + 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; get tickIntervalPercent() { return this._tickIntervalPercent; } + get halfTickIntervalPercent() { return this._tickIntervalPercent / 2; } /** The percentage of the slider that coincides with the value. */ private _percent: number = 0; @@ -167,13 +146,7 @@ export class MdSlider implements ControlValueAccessor { return this._value; } 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; - } - - this._value = Number(v); + this._value = coerceNumberProperty(v, this._value); this._percent = this._calculatePercentage(this._value); this._controlValueAccessorChangeFn(this._value); } @@ -186,14 +159,7 @@ export class MdSlider implements ControlValueAccessor { return this._min; } set min(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; - } - - // 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._value === null) { @@ -210,7 +176,7 @@ export class MdSlider implements ControlValueAccessor { return this._max; } set max(v: number) { - this._max = Number(v); + this._max = coerceNumberProperty(v, this._max); this._percent = this._calculatePercentage(this.value); } @@ -220,31 +186,30 @@ export class MdSlider implements ControlValueAccessor { this._renderer = new SliderRenderer(elementRef); } - /** TODO: internal */ - onMouseenter() { + _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._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._emitValueIfChanged(); } - /** TODO: internal */ - onSlide(event: HammerInput) { + _onSlide(event: HammerInput) { if (this.disabled) { return; } @@ -254,37 +219,25 @@ export class MdSlider implements ControlValueAccessor { 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); } - /** TODO: internal */ - onSlideEnd() { - this.isSliding = false; + _onSlideEnd() { + this._isSliding = false; this._emitValueIfChanged(); } - /** TODO: internal */ - onResize() { - // Consider the slider to be sliding during resize to prevent animation. - this.isSliding = true; - setTimeout(() => { - this.isSliding = false; - }, 0); - } - - /** TODO: internal */ - onBlur() { - this.isActive = false; + _onBlur() { + this._isActive = false; this.onTouched(); } @@ -362,7 +315,6 @@ export class MdSlider implements ControlValueAccessor { /** * Implemented as part of ControlValueAccessor. - * TODO: internal */ writeValue(value: any) { this.value = value; @@ -370,7 +322,6 @@ export class MdSlider implements ControlValueAccessor { /** * Implemented as part of ControlValueAccessor. - * TODO: internal */ registerOnChange(fn: (value: any) => void) { this._controlValueAccessorChangeFn = fn; @@ -378,7 +329,6 @@ export class MdSlider implements ControlValueAccessor { /** * Implemented as part of ControlValueAccessor. - * TODO: internal */ registerOnTouched(fn: any) { this.onTouched = fn; @@ -423,7 +373,7 @@ export class SliderRenderer { @NgModule({ - imports: [FormsModule], + imports: [FormsModule, CommonModule], exports: [MdSlider], declarations: [MdSlider], providers: [ diff --git a/stylelint-config.json b/stylelint-config.json index c50d6a9f6d4b..17f3c7292822 100644 --- a/stylelint-config.json +++ b/stylelint-config.json @@ -29,7 +29,7 @@ "property-case": "lower", "declaration-block-no-duplicate-properties": [ true, { - "ignore": ["consecutive-duplicates"] + "ignore": ["consecutive-duplicates-with-different-values"] }], "declaration-block-no-ignored-properties": true, "declaration-block-trailing-semicolon": "always", From a4ba485dceabef2415b1d448a460140b90c1384c Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 3 Nov 2016 16:21:29 -0700 Subject: [PATCH 07/11] PercentPipe was adding extra space before '%', so replaced it. --- src/lib/slider/slider.html | 8 ++++---- src/lib/slider/slider.ts | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html index 01b6acc917ed..538a371e3c78 100644 --- a/src/lib/slider/slider.html +++ b/src/lib/slider/slider.html @@ -1,8 +1,8 @@
-
-
-
+
+
+
diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index a6bbfd5ab489..e11e4888308d 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -127,7 +127,6 @@ export class MdSlider implements ControlValueAccessor { private _tickIntervalPercent: number = 0; get tickIntervalPercent() { return this._tickIntervalPercent; } - get halfTickIntervalPercent() { return this._tickIntervalPercent / 2; } /** The percentage of the slider that coincides with the value. */ private _percent: number = 0; @@ -180,6 +179,22 @@ export class MdSlider implements ControlValueAccessor { this._percent = this._calculatePercentage(this.value); } + get trackFillFlexBasis() { + return this.percent * 100 + '%'; + } + + get ticksMarginLeft() { + return this.tickIntervalPercent / 2 * 100 + '%'; + } + + get ticksContainerMarginLeft() { + return '-' + this.ticksMarginLeft; + } + + get ticksBackgroundSize() { + return this.tickIntervalPercent * 100 + '% 2px'; + } + @Output() change = new EventEmitter(); constructor(elementRef: ElementRef) { From f3604cf82e9b0cff108498b96aecd6762a00cb33 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 3 Nov 2016 17:15:28 -0700 Subject: [PATCH 08/11] Fixed tests. --- src/lib/slider/slider.spec.ts | 127 +++++++++++++++++----------------- 1 file changed, 63 insertions(+), 64 deletions(-) diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts index 42a2a4c4c201..30fbda1b744a 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -68,35 +68,39 @@ describe('MdSlider', () => { it('should update the value on a click', () => { expect(sliderInstance.value).toBe(0); - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.19); + dispatchClickEventSequence(sliderNativeElement, 0.19); + expect(sliderInstance.value).toBe(19); }); it('should update the value on a slide', () => { expect(sliderInstance.value).toBe(0); - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.89, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.89, gestureConfig); + expect(sliderInstance.value).toBe(89); }); it('should set the value as min when sliding before the track', () => { expect(sliderInstance.value).toBe(0); - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, -1.33, gestureConfig); + 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(sliderTrackElement, sliderNativeElement, 0, 1.75, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 1.75, gestureConfig); + expect(sliderInstance.value).toBe(100); }); it('should update the track fill on click', () => { expect(trackFillElement.style.flexBasis).toBe('0%'); - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.39); + dispatchClickEventSequence(sliderNativeElement, 0.39); fixture.detectChanges(); expect(trackFillElement.style.flexBasis).toBe('39%'); @@ -105,7 +109,7 @@ describe('MdSlider', () => { it('should update the track fill on slide', () => { expect(trackFillElement.style.flexBasis).toBe('0%'); - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.86, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.86, gestureConfig); fixture.detectChanges(); expect(trackFillElement.style.flexBasis).toBe('86%'); @@ -114,14 +118,14 @@ describe('MdSlider', () => { it('should add the md-slider-active class on click', () => { expect(sliderNativeElement.classList).not.toContain('md-slider-active'); - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.23); + dispatchClickEventSequence(sliderNativeElement, 0.23); fixture.detectChanges(); expect(sliderNativeElement.classList).toContain('md-slider-active'); }); it('should remove the md-slider-active class on blur', () => { - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.95); + dispatchClickEventSequence(sliderNativeElement, 0.95); fixture.detectChanges(); expect(sliderNativeElement.classList).toContain('md-slider-active'); @@ -136,12 +140,12 @@ describe('MdSlider', () => { it('should add and remove the md-slider-sliding class when sliding', () => { expect(sliderNativeElement.classList).not.toContain('md-slider-sliding'); - dispatchSlideStartEvent(sliderTrackElement, sliderNativeElement, 0, gestureConfig); + dispatchSlideStartEvent(sliderNativeElement, 0, gestureConfig); fixture.detectChanges(); expect(sliderNativeElement.classList).toContain('md-slider-sliding'); - dispatchSlideEndEvent(sliderTrackElement, sliderNativeElement, 0.34, gestureConfig); + dispatchSlideEndEvent(sliderNativeElement, 0.34, gestureConfig); fixture.detectChanges(); expect(sliderNativeElement.classList).not.toContain('md-slider-sliding'); @@ -172,7 +176,7 @@ describe('MdSlider', () => { it('should not change the value on click when disabled', () => { expect(sliderInstance.value).toBe(0); - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.63); + dispatchClickEventSequence(sliderNativeElement, 0.63); expect(sliderInstance.value).toBe(0); }); @@ -180,7 +184,7 @@ describe('MdSlider', () => { it('should not change the value on slide when disabled', () => { expect(sliderInstance.value).toBe(0); - dispatchSlideEventSequence(sliderNativeElement, sliderNativeElement, 0, 0.5, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.5, gestureConfig); expect(sliderInstance.value).toBe(0); }); @@ -188,7 +192,7 @@ describe('MdSlider', () => { it('should not add the md-slider-active class on click when disabled', () => { expect(sliderNativeElement.classList).not.toContain('md-slider-active'); - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.43); + dispatchClickEventSequence(sliderNativeElement, 0.43); fixture.detectChanges(); expect(sliderNativeElement.classList).not.toContain('md-slider-active'); @@ -197,7 +201,7 @@ describe('MdSlider', () => { it('should not add the md-slider-sliding class on slide when disabled', () => { expect(sliderNativeElement.classList).not.toContain('md-slider-sliding'); - dispatchSlideStartEvent(sliderTrackElement, sliderNativeElement, 0.46, gestureConfig); + dispatchSlideStartEvent(sliderNativeElement, 0.46, gestureConfig); fixture.detectChanges(); expect(sliderNativeElement.classList).not.toContain('md-slider-sliding'); @@ -237,7 +241,7 @@ describe('MdSlider', () => { }); it('should set the correct value on click', () => { - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.09); + dispatchClickEventSequence(sliderNativeElement, 0.09); fixture.detectChanges(); // Computed by multiplying the difference between the min and the max by the percentage from @@ -247,7 +251,7 @@ describe('MdSlider', () => { }); it('should set the correct value on slide', () => { - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.62, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.62, gestureConfig); fixture.detectChanges(); // Computed by multiplying the difference between the min and the max by the percentage from @@ -257,7 +261,7 @@ describe('MdSlider', () => { }); it('should snap the fill to the nearest value on click', () => { - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.68); + dispatchClickEventSequence(sliderNativeElement, 0.68); fixture.detectChanges(); // The closest snap is halfway on the slider. @@ -265,7 +269,7 @@ describe('MdSlider', () => { }); it('should snap the fill to the nearest value on slide', () => { - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.74, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.74, gestureConfig); fixture.detectChanges(); // The closest snap is at the halfway point on the slider. @@ -326,7 +330,7 @@ describe('MdSlider', () => { }); it('should set the correct value on click', () => { - dispatchClickEventSequence(sliderTrackElement, 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 @@ -335,7 +339,7 @@ describe('MdSlider', () => { }); it('should set the correct value on slide', () => { - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.32, gestureConfig); fixture.detectChanges(); expect(sliderInstance.value).toBe(32); @@ -364,14 +368,14 @@ describe('MdSlider', () => { it('should set the correct step value on click', () => { expect(sliderInstance.value).toBe(0); - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.13); + dispatchClickEventSequence(sliderNativeElement, 0.13); fixture.detectChanges(); expect(sliderInstance.value).toBe(25); }); it('should snap the fill to a step on click', () => { - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.66); + dispatchClickEventSequence(sliderNativeElement, 0.66); fixture.detectChanges(); // The closest step is at 75% of the slider. @@ -379,14 +383,14 @@ describe('MdSlider', () => { }); it('should set the correct step value on slide', () => { - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.07, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.07, gestureConfig); fixture.detectChanges(); expect(sliderInstance.value).toBe(0); }); it('should snap the thumb and fill to a step on slide', () => { - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.88, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.88, gestureConfig); fixture.detectChanges(); // The closest snap is at the end of the slider. @@ -481,7 +485,7 @@ describe('MdSlider', () => { it('should update the thumb label text on click', () => { expect(thumbLabelTextElement.textContent).toBe('0'); - dispatchClickEventSequence(sliderTrackElement, 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. @@ -491,7 +495,7 @@ describe('MdSlider', () => { it('should update the thumb label text on slide', () => { expect(thumbLabelTextElement.textContent).toBe('0'); - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.56, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.56, gestureConfig); fixture.detectChanges(); // The thumb label text is set to the slider's value. These should always be the same. @@ -502,7 +506,7 @@ describe('MdSlider', () => { expect(sliderNativeElement.classList).not.toContain('md-slider-active'); expect(sliderNativeElement.classList).toContain('md-slider-thumb-label-showing'); - dispatchClickEventSequence(sliderTrackElement, 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' @@ -514,7 +518,7 @@ describe('MdSlider', () => { it('should show the thumb label on slide', () => { expect(sliderNativeElement.classList).not.toContain('md-slider-active'); - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.91, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.91, gestureConfig); fixture.detectChanges(); expect(sliderNativeElement.classList).toContain('md-slider-thumb-label-showing'); @@ -554,7 +558,7 @@ describe('MdSlider', () => { it('should update the control on click', () => { expect(testComponent.control.value).toBe(0); - dispatchClickEventSequence(sliderTrackElement, sliderNativeElement, 0.76); + dispatchClickEventSequence(sliderNativeElement, 0.76); fixture.detectChanges(); expect(testComponent.control.value).toBe(76); @@ -563,7 +567,7 @@ describe('MdSlider', () => { it('should update the control on slide', () => { expect(testComponent.control.value).toBe(0); - dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.19, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, 0, 0.19, gestureConfig); fixture.detectChanges(); expect(testComponent.control.value).toBe(19); @@ -839,57 +843,55 @@ class SliderWithChangeHandler { } /** - * Dispatches a click event sequence (consisting of moueseenter, click) from an element. + * Dispatches a click 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 sliderElement The md-slider 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 dispatchClickEventSequence(trackElement: HTMLElement, containerElement: 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(containerElement); + dispatchMouseenterEvent(sliderElement); let event = document.createEvent('MouseEvent'); event.initMouseEvent( 'click', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null); - containerElement.dispatchEvent(event); + sliderElement.dispatchEvent(event); } /** * Dispatches a slide event sequence (consisting of slidestart, slide, slideend) from an element. - * @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 sliderElement The md-slider element from which the event will be dispatched. * @param startPercent The percentage of the slider where the slide will begin. * @param endPercent The percentage of the slider where the slide will end. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideEventSequence(trackElement: HTMLElement, containerElement: HTMLElement, - startPercent: number, endPercent: number, - gestureConfig: TestGestureConfig): void { - dispatchSlideStartEvent(trackElement, containerElement, startPercent, gestureConfig); - dispatchSlideEvent(trackElement, containerElement, startPercent, gestureConfig); - dispatchSlideEvent(trackElement, containerElement, endPercent, gestureConfig); - dispatchSlideEndEvent(trackElement, containerElement, endPercent, gestureConfig); +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); + dispatchSlideEndEvent(sliderElement, endPercent, gestureConfig); } /** * Dispatches a slide event from an element. - * @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 sliderElement The md-slider element from which the event will be dispatched. * @param percent The percentage of the slider where the slide will happen. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideEvent(trackElement: HTMLElement, containerElement: HTMLElement, - percent: number, gestureConfig: TestGestureConfig): void { +function dispatchSlideEvent(sliderElement: HTMLElement, percent: number, + gestureConfig: TestGestureConfig): void { + let trackElement = sliderElement.querySelector('.md-slider-track'); let dimensions = trackElement.getBoundingClientRect(); let x = dimensions.left + (dimensions.width * percent); - gestureConfig.emitEventForElement('slide', containerElement, { + gestureConfig.emitEventForElement('slide', sliderElement, { center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); @@ -897,19 +899,17 @@ function dispatchSlideEvent(trackElement: HTMLElement, containerElement: HTMLEle /** * Dispatches a slidestart event from an element. - * @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 sliderElement The md-slider element from which the event will be dispatched. * @param percent The percentage of the slider where the slide will begin. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideStartEvent(trackElement: HTMLElement, containerElement: HTMLElement, - percent: number, gestureConfig: TestGestureConfig): void { +function dispatchSlideStartEvent(sliderElement: HTMLElement, percent: number, + gestureConfig: TestGestureConfig): void { + let trackElement = sliderElement.querySelector('.md-slider-track'); let dimensions = trackElement.getBoundingClientRect(); let x = dimensions.left + (dimensions.width * percent); - dispatchMouseenterEvent(containerElement); - - gestureConfig.emitEventForElement('slidestart', containerElement, { + gestureConfig.emitEventForElement('slidestart', sliderElement, { center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); @@ -917,17 +917,17 @@ function dispatchSlideStartEvent(trackElement: HTMLElement, containerElement: HT /** * Dispatches a slideend event from an element. - * @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 sliderElement The md-slider element from which the event will be dispatched. * @param percent The percentage of the slider where the slide will end. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideEndEvent(trackElement: HTMLElement, containerElement: HTMLElement, - percent: number, gestureConfig: TestGestureConfig): void { +function dispatchSlideEndEvent(sliderElement: HTMLElement, percent: number, + gestureConfig: TestGestureConfig): void { + let trackElement = sliderElement.querySelector('.md-slider-track'); let dimensions = trackElement.getBoundingClientRect(); let x = dimensions.left + (dimensions.width * percent); - gestureConfig.emitEventForElement('slideend', containerElement, { + gestureConfig.emitEventForElement('slideend', sliderElement, { center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); @@ -951,4 +951,3 @@ function dispatchMouseenterEvent(element: HTMLElement): void { 'mouseenter', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null); element.dispatchEvent(event); } - From 565a6f351e475cba69c25bf5b597369cb263f366 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 3 Nov 2016 17:25:12 -0700 Subject: [PATCH 09/11] Fix tab group on slider demo page. --- src/demo-app/slider/slider-demo.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html index e1724ed3a87a..16a5816548a5 100644 --- a/src/demo-app/slider/slider-demo.html +++ b/src/demo-app/slider/slider-demo.html @@ -35,10 +35,7 @@

Slider with two-way binding

- - - + + \ No newline at end of file From d5fb4e75a352e02027eb0c706c8d11619b49613e Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 4 Nov 2016 15:29:58 -0700 Subject: [PATCH 10/11] remove CommonModule from imports. --- src/lib/slider/slider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index e11e4888308d..eed6f171ff57 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -13,7 +13,6 @@ import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/for import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core'; import {Input as HammerInput} from 'hammerjs'; -import {CommonModule} from '@angular/common'; /** * Visually, a 30px separation between tick marks looks best. This is very subjective but it is @@ -388,7 +387,7 @@ export class SliderRenderer { @NgModule({ - imports: [FormsModule, CommonModule], + imports: [FormsModule], exports: [MdSlider], declarations: [MdSlider], providers: [ From c48f7d6a74ff7e295c5be867885218bbda458d4e Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 7 Nov 2016 17:20:52 -0800 Subject: [PATCH 11/11] Addressed comments. --- src/lib/core/coersion/number-property.spec.ts | 5 +++++ src/lib/core/coersion/number-property.ts | 5 ++++- src/lib/slider/slider.spec.ts | 6 +++--- src/lib/slider/slider.ts | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/lib/core/coersion/number-property.spec.ts b/src/lib/core/coersion/number-property.spec.ts index 7d06f481de61..fb0edfd2e544 100644 --- a/src/lib/core/coersion/number-property.spec.ts +++ b/src/lib/core/coersion/number-property.spec.ts @@ -49,6 +49,11 @@ describe('coerceNumberProperty', () => { expect(coerceNumberProperty('pink', 111)).toBe(111); }); + it('should coerce an arbitrary string prefixed with a number to 0 or default', () => { + expect(coerceNumberProperty('123pink')).toBe(0); + expect(coerceNumberProperty('123pink', 111)).toBe(111); + }); + it('should coerce the number 1 to 1', () => { expect(coerceNumberProperty(1)).toBe(1); expect(coerceNumberProperty(1, 111)).toBe(1); diff --git a/src/lib/core/coersion/number-property.ts b/src/lib/core/coersion/number-property.ts index 708bea97270a..c23b74316d43 100644 --- a/src/lib/core/coersion/number-property.ts +++ b/src/lib/core/coersion/number-property.ts @@ -1,4 +1,7 @@ /** Coerces a data-bound value (typically a string) to a number. */ export function coerceNumberProperty(value: any, fallbackValue = 0) { - return isNaN(parseFloat(value as any)) ? fallbackValue : Number(value); + // parseFloat(value) handles most of the cases we're interested in (it treats null, empty string, + // and other non-number values as NaN, where Number just uses 0) but it considers the string + // '123hello' to be a valid number. Therefore we also check if Number(value) is NaN. + return isNaN(parseFloat(value as any)) || isNaN(Number(value)) ? fallbackValue : Number(value); } diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts index 30fbda1b744a..320b54a9e5b2 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -546,13 +546,13 @@ 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', () => { @@ -819,7 +819,7 @@ class SliderWithOneWayBinding { styles: [styles], }) class SliderWithTwoWayBinding { - control = new FormControl(''); + control = new FormControl(0); } @Component({ diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index eed6f171ff57..ac7f0c6912bc 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -146,7 +146,6 @@ export class MdSlider implements ControlValueAccessor { set value(v: number) { this._value = coerceNumberProperty(v, this._value); this._percent = this._calculatePercentage(this._value); - this._controlValueAccessorChangeFn(this._value); } /** The miniumum value that the slider can have. */ @@ -284,6 +283,7 @@ export class MdSlider implements ControlValueAccessor { event.source = this; event.value = this.value; this.change.emit(event); + this._controlValueAccessorChangeFn(this.value); this._lastEmittedValue = this.value; } }