mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(slider): vertical mode (#1878)
* Addressed comments. * PercentPipe was adding extra space before '%', so replaced it. * remove CommonModule from imports. * fix(slider): keyboard support. * prevent keyboard interaction with disabled slider. * fix comment * switch to event.keyCode * added tests * comment why default: return; * fix(slider): support for rtl and inverted sliders. * clean up demo html file * fixed tests and lint issues * added tests * started work * swap left/right arrow behavior in rtl * fixed lint issues * sorta working * works (except end tick is sometimes hidden). all the tests are probably broken T-T * fix tick glitches * all translate, no margin for tick placement * make sure the last tick appears on top of fill * fix tests * fixed test issues on mobile * fix mobile bug where slide doesn't work until after click * swap the meaning of normal and inverted for vertical sliders * vertical mode tests * fix vertical slider in ie11 * fixed lint issue
This commit is contained in:
@@ -35,7 +35,13 @@ Label <md-slider #slidey></md-slider>
|
||||
<input [(ngModel)]="demo">
|
||||
|
||||
<h1>Inverted slider</h1>
|
||||
<md-slider invert value="50"></md-slider>
|
||||
<md-slider invert value="50" tick-interval="5"></md-slider>
|
||||
|
||||
<h1>Vertical slider</h1>
|
||||
<md-slider vertical thumb-label tick-interval="auto" value="50"></md-slider>
|
||||
|
||||
<h1>Inverted vertical slider</h1>
|
||||
<md-slider vertical invert thumb-label tick-interval="auto" value="50"></md-slider>
|
||||
|
||||
<md-tab-group>
|
||||
<md-tab label="One">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="md-slider-ticks-container" [ngStyle]="ticksContainerStyles">
|
||||
<div class="md-slider-ticks" [ngStyle]="ticksStyles"></div>
|
||||
</div>
|
||||
<div class="md-slider-thumb-container">
|
||||
<div class="md-slider-thumb-container" [ngStyle]="thumbContainerStyles">
|
||||
<div class="md-slider-thumb"></div>
|
||||
<div class="md-slider-thumb-label">
|
||||
<span class="md-slider-thumb-label-text">{{value}}</span>
|
||||
|
||||
+224
-83
@@ -23,142 +23,283 @@ $md-slider-tick-size: 2px !default;
|
||||
|
||||
md-slider {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
height: $md-slider-thickness;
|
||||
min-width: $md-slider-min-size;
|
||||
box-sizing: border-box;
|
||||
padding: $md-slider-padding;
|
||||
outline: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
[dir='rtl'] .md-slider-has-ticks.md-slider-active .md-slider-track,
|
||||
[dir='rtl'] .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;
|
||||
}
|
||||
|
||||
.md-slider-inverted .md-slider-track {
|
||||
flex-direction: row-reverse;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.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-sliding .md-slider-track-fill {
|
||||
transition: none;
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
|
||||
}
|
||||
|
||||
.md-slider-ticks-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: $md-slider-track-thickness;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[dir='rtl'] .md-slider-ticks-container {
|
||||
// translateZ(0) prevents chrome bug where overflow: hidden; doesn't work.
|
||||
transform: translateZ(0) rotate(180deg);
|
||||
}
|
||||
|
||||
.md-slider-ticks {
|
||||
background: repeating-linear-gradient(to right, $md-slider-tick-color,
|
||||
$md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;
|
||||
// Firefox doesn't draw the gradient correctly with 'to right'
|
||||
// (see https://bugzilla.mozilla.org/show_bug.cgi?id=1314319).
|
||||
background: -moz-repeating-linear-gradient(0.0001deg, $md-slider-tick-color,
|
||||
$md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;
|
||||
height: $md-slider-track-thickness;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity $swift-ease-out-duration $swift-ease-out-timing-function;
|
||||
}
|
||||
|
||||
.md-slider-has-ticks.md-slider-active .md-slider-ticks,
|
||||
.md-slider-has-ticks:hover .md-slider-ticks {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.md-slider-thumb-container {
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function;
|
||||
}
|
||||
|
||||
.md-slider-thumb {
|
||||
position: absolute;
|
||||
left: -$md-slider-thumb-size / 2;
|
||||
top: -$md-slider-thumb-size / 2;
|
||||
right: -$md-slider-thumb-size / 2;
|
||||
bottom: -$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-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;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
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: 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;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
transform: rotate(-45deg);
|
||||
transition: opacity 300ms $swift-ease-in-out-timing-function;
|
||||
}
|
||||
|
||||
.md-slider-active .md-slider-thumb-label-text {
|
||||
opacity: 1;
|
||||
|
||||
// Slider sliding state.
|
||||
.md-slider-sliding {
|
||||
.md-slider-track-fill,
|
||||
.md-slider-thumb-container {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Slider with ticks.
|
||||
.md-slider-has-ticks {
|
||||
.md-slider-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 0 solid $md-slider-tick-color;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms $swift-ease-in-out-timing-function;
|
||||
}
|
||||
|
||||
&.md-slider-active,
|
||||
&:hover {
|
||||
.md-slider-track::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.md-slider-ticks {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Slider with thumb label.
|
||||
.md-slider-thumb-label-showing {
|
||||
.md-slider-thumb-label {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Inverted slider.
|
||||
.md-slider-axis-inverted {
|
||||
.md-slider-track-fill {
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Active slider.
|
||||
.md-slider-active {
|
||||
.md-slider-thumb {
|
||||
transform: scale($md-slider-thumb-focus-scale);
|
||||
}
|
||||
|
||||
&.md-slider-thumb-label-showing .md-slider-thumb {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.md-slider-thumb-label {
|
||||
border-radius: 50% 50% 0;
|
||||
}
|
||||
|
||||
.md-slider-thumb-label-text {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Horizontal slider.
|
||||
.md-slider-horizontal {
|
||||
height: $md-slider-thickness;
|
||||
min-width: $md-slider-min-size;
|
||||
|
||||
.md-slider-track {
|
||||
height: $md-slider-track-thickness;
|
||||
top: ($md-slider-thickness - $md-slider-track-thickness) / 2;
|
||||
left: $md-slider-padding;
|
||||
right: $md-slider-padding;
|
||||
}
|
||||
|
||||
.md-slider-track::after {
|
||||
height: $md-slider-track-thickness;
|
||||
border-left-width: $md-slider-tick-size;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.md-slider-track-fill {
|
||||
height: $md-slider-track-thickness;
|
||||
width: 100%;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
.md-slider-ticks-container {
|
||||
height: $md-slider-track-thickness;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md-slider-ticks {
|
||||
background: repeating-linear-gradient(to right, $md-slider-tick-color,
|
||||
$md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;
|
||||
// Firefox doesn't draw the gradient correctly with 'to right'
|
||||
// (see https://bugzilla.mozilla.org/show_bug.cgi?id=1314319).
|
||||
background: -moz-repeating-linear-gradient(0.0001deg, $md-slider-tick-color,
|
||||
$md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;
|
||||
height: $md-slider-track-thickness;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md-slider-thumb-container {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.md-slider-thumb-label {
|
||||
right: -$md-slider-thumb-label-size / 2;
|
||||
top: -($md-slider-thumb-label-size + $md-slider-thumb-arrow-gap);
|
||||
transform: translateY($md-slider-thumb-label-size / 2 + $md-slider-thumb-arrow-gap) scale(0.4)
|
||||
rotate(45deg);
|
||||
}
|
||||
|
||||
.md-slider-thumb-label-text {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&.md-slider-active {
|
||||
.md-slider-thumb-label {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Vertical slider.
|
||||
.md-slider-vertical {
|
||||
width: $md-slider-thickness;
|
||||
min-height: $md-slider-min-size;
|
||||
|
||||
.md-slider-track {
|
||||
width: $md-slider-track-thickness;
|
||||
top: $md-slider-padding;
|
||||
bottom: $md-slider-padding;
|
||||
left: ($md-slider-thickness - $md-slider-track-thickness) / 2;
|
||||
}
|
||||
|
||||
.md-slider-track::after {
|
||||
width: $md-slider-track-thickness;
|
||||
border-top-width: $md-slider-tick-size;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.md-slider-track-fill {
|
||||
height: 100%;
|
||||
width: $md-slider-track-thickness;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
|
||||
.md-slider-ticks-container {
|
||||
width: $md-slider-track-thickness;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.md-slider-ticks {
|
||||
background: repeating-linear-gradient(to bottom, $md-slider-tick-color,
|
||||
$md-slider-tick-color $md-slider-tick-size, transparent 0, transparent) repeat;
|
||||
width: $md-slider-track-thickness;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.md-slider-thumb-container {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.md-slider-thumb-label {
|
||||
bottom: -$md-slider-thumb-label-size / 2;
|
||||
left: -($md-slider-thumb-label-size + $md-slider-thumb-arrow-gap);
|
||||
transform: translateX($md-slider-thumb-label-size / 2 + $md-slider-thumb-arrow-gap)
|
||||
scale(0.4) rotate(-45deg);
|
||||
}
|
||||
|
||||
.md-slider-thumb-label-text {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&.md-slider-active {
|
||||
.md-slider-thumb-label {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Slider in RTL languages.
|
||||
[dir='rtl'] {
|
||||
.md-slider-track::after {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.md-slider-horizontal {
|
||||
.md-slider-track-fill {
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
|
||||
&.md-slider-axis-inverted {
|
||||
.md-slider-track-fill {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+118
-45
@@ -38,6 +38,7 @@ describe('MdSlider', () => {
|
||||
SliderWithValueGreaterThanMax,
|
||||
SliderWithChangeHandler,
|
||||
SliderWithDirAndInvert,
|
||||
VerticalSlider,
|
||||
],
|
||||
providers: [
|
||||
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
|
||||
@@ -109,21 +110,21 @@ describe('MdSlider', () => {
|
||||
});
|
||||
|
||||
it('should update the track fill on click', () => {
|
||||
expect(trackFillElement.style.flexBasis).toBe('0%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0)');
|
||||
|
||||
dispatchClickEventSequence(sliderNativeElement, 0.39);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(trackFillElement.style.flexBasis).toBe('39%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.39)');
|
||||
});
|
||||
|
||||
it('should update the track fill on slide', () => {
|
||||
expect(trackFillElement.style.flexBasis).toBe('0%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0)');
|
||||
|
||||
dispatchSlideEventSequence(sliderNativeElement, 0, 0.86, gestureConfig);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(trackFillElement.style.flexBasis).toBe('86%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.86)');
|
||||
});
|
||||
|
||||
it('should add the md-slider-active class on click', () => {
|
||||
@@ -276,7 +277,7 @@ describe('MdSlider', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// The closest snap is halfway on the slider.
|
||||
expect(trackFillElement.style.flexBasis).toBe('50%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.5)');
|
||||
});
|
||||
|
||||
it('should snap the fill to the nearest value on slide', () => {
|
||||
@@ -284,7 +285,7 @@ describe('MdSlider', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// The closest snap is at the halfway point on the slider.
|
||||
expect(trackFillElement.style.flexBasis).toBe('50%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.5)');
|
||||
});
|
||||
|
||||
it('should adjust fill and ticks on mouse enter when min changes', () => {
|
||||
@@ -294,11 +295,11 @@ describe('MdSlider', () => {
|
||||
dispatchMouseenterEvent(sliderNativeElement);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(trackFillElement.style.flexBasis).toBe('75%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.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%');
|
||||
expect(ticksElement.style.transform).toContain('translateX(37.5%)');
|
||||
expect(ticksContainerElement.style.transform).toBe('translateX(-37.5%)');
|
||||
});
|
||||
|
||||
it('should adjust fill and ticks on mouse enter when max changes', () => {
|
||||
@@ -311,11 +312,11 @@ describe('MdSlider', () => {
|
||||
dispatchMouseenterEvent(sliderNativeElement);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(trackFillElement.style.flexBasis).toBe('50%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.5)');
|
||||
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%');
|
||||
expect(ticksElement.style.transform).toContain('translateX(25%)');
|
||||
expect(ticksContainerElement.style.transform).toBe('translateX(-25%)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -390,7 +391,7 @@ describe('MdSlider', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// The closest step is at 75% of the slider.
|
||||
expect(trackFillElement.style.flexBasis).toBe('75%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.75)');
|
||||
});
|
||||
|
||||
it('should set the correct step value on slide', () => {
|
||||
@@ -405,7 +406,7 @@ describe('MdSlider', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// The closest snap is at the end of the slider.
|
||||
expect(trackFillElement.style.flexBasis).toBe('100%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(1)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -434,8 +435,8 @@ describe('MdSlider', () => {
|
||||
// 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%');
|
||||
expect(ticksElement.style.transform).toContain('translateX(15%)');
|
||||
expect(ticksContainerElement.style.transform).toBe('translateX(-15%)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -465,8 +466,8 @@ describe('MdSlider', () => {
|
||||
// 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%');
|
||||
expect(ticksElement.style.transform).toContain('translateX(9%)');
|
||||
expect(ticksContainerElement.style.transform).toBe('translateX(-9%)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -638,7 +639,7 @@ describe('MdSlider', () => {
|
||||
|
||||
it('should initialize based on bound value', () => {
|
||||
expect(sliderInstance.value).toBe(50);
|
||||
expect(trackFillElement.style.flexBasis).toBe('50%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.5)');
|
||||
});
|
||||
|
||||
it('should update when bound value changes', () => {
|
||||
@@ -646,7 +647,7 @@ describe('MdSlider', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(75);
|
||||
expect(trackFillElement.style.flexBasis).toBe('75%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0.75)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -676,7 +677,7 @@ describe('MdSlider', () => {
|
||||
});
|
||||
|
||||
it('should set the fill to the min value', () => {
|
||||
expect(trackFillElement.style.flexBasis).toBe('0%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(0)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -706,7 +707,7 @@ describe('MdSlider', () => {
|
||||
});
|
||||
|
||||
it('should set the fill to the max value', () => {
|
||||
expect(trackFillElement.style.flexBasis).toBe('100%');
|
||||
expect(trackFillElement.style.transform).toBe('scaleX(1)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -892,25 +893,25 @@ describe('MdSlider', () => {
|
||||
expect(sliderInstance.value).toBe(30);
|
||||
});
|
||||
|
||||
it('should decrement inverted slider by 1 on right arrow pressed', () => {
|
||||
it('should increment inverted slider by 1 on right arrow pressed', () => {
|
||||
testComponent.invert = true;
|
||||
sliderInstance.value = 100;
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(99);
|
||||
expect(sliderInstance.value).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment inverted slider by 1 on left arrow pressed', () => {
|
||||
it('should decrement inverted slider by 1 on left arrow pressed', () => {
|
||||
testComponent.invert = true;
|
||||
sliderInstance.value = 100;
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(1);
|
||||
expect(sliderInstance.value).toBe(99);
|
||||
});
|
||||
|
||||
it('should decrement RTL slider by 1 on right arrow pressed', () => {
|
||||
@@ -934,35 +935,96 @@ describe('MdSlider', () => {
|
||||
expect(sliderInstance.value).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment inverted RTL slider by 1 on right arrow pressed', () => {
|
||||
testComponent.dir = 'rtl';
|
||||
testComponent.invert = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(1);
|
||||
});
|
||||
|
||||
it('should decrement inverted RTL slider by 1 on left arrow pressed', () => {
|
||||
it('should decrement inverted RTL slider by 1 on right arrow pressed', () => {
|
||||
testComponent.dir = 'rtl';
|
||||
testComponent.invert = true;
|
||||
sliderInstance.value = 100;
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
|
||||
dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(99);
|
||||
});
|
||||
|
||||
it('should increment inverted RTL slider by 1 on left arrow pressed', () => {
|
||||
testComponent.dir = 'rtl';
|
||||
testComponent.invert = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vertical slider', () => {
|
||||
let fixture: ComponentFixture<VerticalSlider>;
|
||||
let sliderDebugElement: DebugElement;
|
||||
let sliderNativeElement: HTMLElement;
|
||||
let sliderTrackElement: HTMLElement;
|
||||
let trackFillElement: HTMLElement;
|
||||
let sliderInstance: MdSlider;
|
||||
let testComponent: VerticalSlider;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(VerticalSlider);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
|
||||
sliderInstance = sliderDebugElement.injector.get(MdSlider);
|
||||
sliderNativeElement = sliderDebugElement.nativeElement;
|
||||
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
|
||||
trackFillElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track-fill');
|
||||
});
|
||||
|
||||
it('updates value on click', () => {
|
||||
dispatchClickEventSequence(sliderNativeElement, 0.3);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(70);
|
||||
});
|
||||
|
||||
it('updates value on click in inverted mode', () => {
|
||||
testComponent.invert = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchClickEventSequence(sliderNativeElement, 0.3);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(sliderInstance.value).toBe(30);
|
||||
});
|
||||
|
||||
it('should update the track fill on click', () => {
|
||||
expect(trackFillElement.style.transform).toBe('scaleY(0)');
|
||||
|
||||
dispatchClickEventSequence(sliderNativeElement, 0.39);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(trackFillElement.style.transform).toBe('scaleY(0.61)');
|
||||
});
|
||||
|
||||
it('should update the track fill on click in inverted mode', () => {
|
||||
testComponent.invert = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(trackFillElement.style.transform).toBe('scaleY(0)');
|
||||
|
||||
dispatchClickEventSequence(sliderNativeElement, 0.39);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(trackFillElement.style.transform).toBe('scaleY(0.39)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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-horizontal { min-width: 116px !important; }
|
||||
.md-slider-vertical { min-height: 116px !important; }
|
||||
.md-slider-track-fill { transition: none !important; }
|
||||
`;
|
||||
|
||||
@@ -1062,6 +1124,14 @@ class SliderWithDirAndInvert {
|
||||
invert = false;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<md-slider vertical [invert]="invert"></md-slider>`,
|
||||
styles: [styles],
|
||||
})
|
||||
class VerticalSlider {
|
||||
invert = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a click event sequence (consisting of moueseenter, click) from an element.
|
||||
* Note: The mouse event truncates the position for the click.
|
||||
@@ -1072,8 +1142,8 @@ class SliderWithDirAndInvert {
|
||||
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);
|
||||
let y = dimensions.top + (dimensions.height * percentage);
|
||||
|
||||
dispatchMouseenterEvent(sliderElement);
|
||||
|
||||
@@ -1110,9 +1180,10 @@ function dispatchSlideEvent(sliderElement: HTMLElement, percent: number,
|
||||
let trackElement = sliderElement.querySelector('.md-slider-track');
|
||||
let dimensions = trackElement.getBoundingClientRect();
|
||||
let x = dimensions.left + (dimensions.width * percent);
|
||||
let y = dimensions.top + (dimensions.height * percent);
|
||||
|
||||
gestureConfig.emitEventForElement('slide', sliderElement, {
|
||||
center: { x: x },
|
||||
center: { x: x, y: y },
|
||||
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
|
||||
});
|
||||
}
|
||||
@@ -1128,11 +1199,12 @@ function dispatchSlideStartEvent(sliderElement: HTMLElement, percent: number,
|
||||
let trackElement = sliderElement.querySelector('.md-slider-track');
|
||||
let dimensions = trackElement.getBoundingClientRect();
|
||||
let x = dimensions.left + (dimensions.width * percent);
|
||||
let y = dimensions.top + (dimensions.height * percent);
|
||||
|
||||
dispatchMouseenterEvent(sliderElement);
|
||||
|
||||
gestureConfig.emitEventForElement('slidestart', sliderElement, {
|
||||
center: { x: x },
|
||||
center: { x: x, y: y },
|
||||
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
|
||||
});
|
||||
}
|
||||
@@ -1148,9 +1220,10 @@ function dispatchSlideEndEvent(sliderElement: HTMLElement, percent: number,
|
||||
let trackElement = sliderElement.querySelector('.md-slider-track');
|
||||
let dimensions = trackElement.getBoundingClientRect();
|
||||
let x = dimensions.left + (dimensions.width * percent);
|
||||
let y = dimensions.top + (dimensions.height * percent);
|
||||
|
||||
gestureConfig.emitEventForElement('slideend', sliderElement, {
|
||||
center: { x: x },
|
||||
center: { x: x, y: y },
|
||||
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
|
||||
});
|
||||
}
|
||||
|
||||
+86
-30
@@ -76,9 +76,11 @@ export class MdSliderChange {
|
||||
'[class.md-slider-active]': '_isActive',
|
||||
'[class.md-slider-disabled]': 'disabled',
|
||||
'[class.md-slider-has-ticks]': 'tickInterval',
|
||||
'[class.md-slider-inverted]': 'invert',
|
||||
'[class.md-slider-horizontal]': '!vertical',
|
||||
'[class.md-slider-axis-inverted]': 'invertAxis',
|
||||
'[class.md-slider-sliding]': '_isSliding',
|
||||
'[class.md-slider-thumb-label-showing]': 'thumbLabel',
|
||||
'[class.md-slider-vertical]': 'vertical',
|
||||
},
|
||||
templateUrl: 'slider.html',
|
||||
styleUrls: ['slider.css'],
|
||||
@@ -202,34 +204,80 @@ export class MdSlider implements ControlValueAccessor {
|
||||
/** Whether the slider is inverted. */
|
||||
@Input()
|
||||
get invert() { return this._invert; }
|
||||
set invert(value: boolean) { this._invert = coerceBooleanProperty(value); }
|
||||
set invert(value: any) { this._invert = coerceBooleanProperty(value); }
|
||||
private _invert = false;
|
||||
|
||||
/** Whether the slider is vertical. */
|
||||
@Input()
|
||||
get vertical() { return this._vertical; }
|
||||
set vertical(value: any) { this._vertical = coerceBooleanProperty(value); }
|
||||
private _vertical = false;
|
||||
|
||||
/**
|
||||
* Whether the axis of the slider is inverted.
|
||||
* (i.e. whether moving the thumb in the positive x or y direction decreases the slider's value).
|
||||
*/
|
||||
get invertAxis() {
|
||||
// Standard non-inverted mode for a vertical slider should be dragging the thumb from bottom to
|
||||
// top. However from a y-axis standpoint this is inverted.
|
||||
return this.vertical ? !this.invert : this.invert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether mouse events should be converted to a slider position by calculating their distance
|
||||
* from the right or bottom edge of the slider as opposed to the top or left.
|
||||
*/
|
||||
get invertMouseCoords() {
|
||||
return (this.direction == 'rtl' && !this.vertical) ? !this.invertAxis : this.invertAxis;
|
||||
}
|
||||
|
||||
/** CSS styles for the track fill element. */
|
||||
get trackFillStyles(): { [key: string]: string } {
|
||||
let axis = this.vertical ? 'Y' : 'X';
|
||||
return {
|
||||
'flexBasis': `${this.percent * 100}%`
|
||||
'transform': `scale${axis}(${this.percent})`
|
||||
};
|
||||
}
|
||||
|
||||
/** CSS styles for the ticks container element. */
|
||||
get ticksContainerStyles(): { [key: string]: string } {
|
||||
let axis = this.vertical ? 'Y' : 'X';
|
||||
// For a horizontal slider in RTL languages we push the ticks container off the left edge
|
||||
// instead of the right edge to avoid causing a horizontal scrollbar to appear.
|
||||
let sign = !this.vertical && this.direction == 'rtl' ? '' : '-';
|
||||
let offset = this.tickIntervalPercent / 2 * 100;
|
||||
return {
|
||||
'marginLeft': `${this.direction == 'rtl' ? '' : '-'}${this.tickIntervalPercent / 2 * 100}%`
|
||||
'transform': `translate${axis}(${sign}${offset}%)`
|
||||
};
|
||||
}
|
||||
|
||||
/** CSS styles for the ticks element. */
|
||||
get ticksStyles() {
|
||||
let styles: { [key: string]: string } = {
|
||||
'backgroundSize': `${this.tickIntervalPercent * 100}% 2px`
|
||||
get ticksStyles(): { [key: string]: string } {
|
||||
let tickSize = this.tickIntervalPercent * 100;
|
||||
let backgroundSize = this.vertical ? `2px ${tickSize}%` : `${tickSize}% 2px`;
|
||||
let axis = this.vertical ? 'Y' : 'X';
|
||||
// Depending on the direction we pushed the ticks container, push the ticks the opposite
|
||||
// direction to re-center them but clip off the end edge. In RTL languages we need to flip the
|
||||
// ticks 180 degrees so we're really cutting off the end edge abd not the start.
|
||||
let sign = !this.vertical && this.direction == 'rtl' ? '-' : '';
|
||||
let rotate = !this.vertical && this.direction == 'rtl' ? ' rotate(180deg)' : '';
|
||||
return {
|
||||
'backgroundSize': backgroundSize,
|
||||
// Without translateZ ticks sometimes jitter as the slider moves on Chrome & Firefox.
|
||||
'transform': `translateZ(0) translate${axis}(${sign}${tickSize / 2}%)${rotate}`
|
||||
};
|
||||
}
|
||||
|
||||
get thumbContainerStyles(): { [key: string]: string } {
|
||||
let axis = this.vertical ? 'Y' : 'X';
|
||||
// For a horizontal slider in RTL languages we push the thumb container off the left edge
|
||||
// instead of the right edge to avoid causing a horizontal scrollbar to appear.
|
||||
let invertOffset =
|
||||
(this.direction == 'rtl' && !this.vertical) ? !this.invertAxis : this.invertAxis;
|
||||
let offset = (invertOffset ? this.percent : 1 - this.percent) * 100;
|
||||
return {
|
||||
'transform': `translate${axis}(-${offset}%)`
|
||||
};
|
||||
if (this.direction == 'rtl') {
|
||||
styles['marginRight'] = `-${this.tickIntervalPercent / 2 * 100}%`;
|
||||
} else {
|
||||
styles['marginLeft'] = `${this.tickIntervalPercent / 2 * 100}%`;
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
/** The language direction for this slider element. */
|
||||
@@ -262,7 +310,7 @@ export class MdSlider implements ControlValueAccessor {
|
||||
this._isActive = true;
|
||||
this._isSliding = false;
|
||||
this._renderer.addFocus();
|
||||
this._updateValueFromPosition(event.clientX);
|
||||
this._updateValueFromPosition({x: event.clientX, y: event.clientY});
|
||||
this._emitValueIfChanged();
|
||||
}
|
||||
|
||||
@@ -273,7 +321,7 @@ export class MdSlider implements ControlValueAccessor {
|
||||
|
||||
// Prevent the slide from selecting anything else.
|
||||
event.preventDefault();
|
||||
this._updateValueFromPosition(event.center.x);
|
||||
this._updateValueFromPosition({x: event.center.x, y: event.center.y});
|
||||
}
|
||||
|
||||
_onSlideStart(event: HammerInput) {
|
||||
@@ -281,11 +329,14 @@ export class MdSlider implements ControlValueAccessor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate mouseenter in case this is a mobile device.
|
||||
this._onMouseenter();
|
||||
|
||||
event.preventDefault();
|
||||
this._isSliding = true;
|
||||
this._isActive = true;
|
||||
this._renderer.addFocus();
|
||||
this._updateValueFromPosition(event.center.x);
|
||||
this._updateValueFromPosition({x: event.center.x, y: event.center.y});
|
||||
}
|
||||
|
||||
_onSlideEnd() {
|
||||
@@ -315,13 +366,21 @@ export class MdSlider implements ControlValueAccessor {
|
||||
this.value = this.min;
|
||||
break;
|
||||
case LEFT_ARROW:
|
||||
this._increment(this._isLeftMin() ? -1 : 1);
|
||||
// NOTE: For a sighted user it would make more sense that when they press an arrow key on an
|
||||
// inverted slider the thumb moves in that direction. However for a blind user, nothing
|
||||
// about the slider indicates that it is inverted. They will expect left to be decrement,
|
||||
// regardless of how it appears on the screen. For speakers ofRTL languages, they probably
|
||||
// expect left to mean increment. Therefore we flip the meaning of the side arrow keys for
|
||||
// RTL. For inverted sliders we prefer a good a11y experience to having it "look right" for
|
||||
// sighted users, therefore we do not swap the meaning.
|
||||
this._increment(this.direction == 'rtl' ? 1 : -1);
|
||||
break;
|
||||
case UP_ARROW:
|
||||
this._increment(1);
|
||||
break;
|
||||
case RIGHT_ARROW:
|
||||
this._increment(this._isLeftMin() ? 1 : -1);
|
||||
// See comment on LEFT_ARROW about the conditions under which we flip the meaning.
|
||||
this._increment(this.direction == 'rtl' ? -1 : 1);
|
||||
break;
|
||||
case DOWN_ARROW:
|
||||
this._increment(-1);
|
||||
@@ -335,11 +394,6 @@ export class MdSlider implements ControlValueAccessor {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/** Whether the left side of the slider is the minimum value. */
|
||||
private _isLeftMin() {
|
||||
return (this.direction == 'rtl') == this.invert;
|
||||
}
|
||||
|
||||
/** Increments the slider by the given number of steps (negative number decrements). */
|
||||
private _increment(numSteps: number) {
|
||||
this.value = this._clamp(this.value + this.step * numSteps, this.min, this.max);
|
||||
@@ -348,17 +402,18 @@ export class MdSlider implements ControlValueAccessor {
|
||||
/**
|
||||
* Calculate the new value from the new physical location. The value will always be snapped.
|
||||
*/
|
||||
private _updateValueFromPosition(pos: number) {
|
||||
private _updateValueFromPosition(pos: {x: number, y: number}) {
|
||||
if (!this._sliderDimensions) {
|
||||
return;
|
||||
}
|
||||
|
||||
let offset = this._sliderDimensions.left;
|
||||
let size = this._sliderDimensions.width;
|
||||
let offset = this.vertical ? this._sliderDimensions.top : this._sliderDimensions.left;
|
||||
let size = this.vertical ? this._sliderDimensions.height : this._sliderDimensions.width;
|
||||
let posComponent = this.vertical ? pos.y : pos.x;
|
||||
|
||||
// The exact value is calculated from the event and used to find the closest snap value.
|
||||
let percent = this._clamp((pos - offset) / size);
|
||||
if (!this._isLeftMin()) {
|
||||
let percent = this._clamp((posComponent - offset) / size);
|
||||
if (this.invertMouseCoords) {
|
||||
percent = 1 - percent;
|
||||
}
|
||||
let exactValue = this._calculateValue(percent);
|
||||
@@ -391,10 +446,11 @@ export class MdSlider implements ControlValueAccessor {
|
||||
}
|
||||
|
||||
if (this.tickInterval == 'auto') {
|
||||
let pixelsPerStep = this._sliderDimensions.width * this.step / (this.max - this.min);
|
||||
let trackSize = this.vertical ? this._sliderDimensions.height : this._sliderDimensions.width;
|
||||
let pixelsPerStep = trackSize * 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);
|
||||
this._tickIntervalPercent = pixelsPerTick / trackSize;
|
||||
} else {
|
||||
this._tickIntervalPercent = this.tickInterval * this.step / (this.max - this.min);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user