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:
mmalerba
2016-12-02 13:33:20 -08:00
committed by tinayuangao
parent 4d678d4f03
commit deb940f030
5 changed files with 436 additions and 160 deletions
+7 -1
View File
@@ -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">
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}