diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss
index a8ae53908b05..2b2c3f36f860 100644
--- a/src/lib/slider/slider.scss
+++ b/src/lib/slider/slider.scss
@@ -47,6 +47,15 @@ md-slider {
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;
+}
+
.md-slider-track-fill {
flex: 0 0 50%;
height: $md-slider-track-thickness;
@@ -66,6 +75,11 @@ md-slider {
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;
diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts
index ef635cd58e56..b22ebc66c76d 100644
--- a/src/lib/slider/slider.spec.ts
+++ b/src/lib/slider/slider.spec.ts
@@ -4,6 +4,7 @@ import {Component, DebugElement} from '@angular/core';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {MdSlider, MdSliderModule} from './slider';
import {TestGestureConfig} from './test-gesture-config';
+import {RtlModule} from '../core/rtl/dir';
import {
UP_ARROW,
RIGHT_ARROW,
@@ -11,7 +12,8 @@ import {
PAGE_DOWN,
PAGE_UP,
END,
- HOME, LEFT_ARROW
+ HOME,
+ LEFT_ARROW
} from '../core/keyboard/keycodes';
@@ -20,7 +22,7 @@ describe('MdSlider', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MdSliderModule.forRoot(), ReactiveFormsModule],
+ imports: [MdSliderModule.forRoot(), RtlModule.forRoot(), ReactiveFormsModule],
declarations: [
StandardSlider,
DisabledSlider,
@@ -35,6 +37,7 @@ describe('MdSlider', () => {
SliderWithValueSmallerThanMin,
SliderWithValueGreaterThanMax,
SliderWithChangeHandler,
+ SliderWithDirAndInvert,
],
providers: [
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
@@ -838,6 +841,122 @@ describe('MdSlider', () => {
expect(sliderInstance.value).toBe(0);
});
});
+
+ describe('slider with direction and invert', () => {
+ let fixture: ComponentFixture
;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderTrackElement: HTMLElement;
+ let sliderInstance: MdSlider;
+ let testComponent: SliderWithDirAndInvert;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SliderWithDirAndInvert);
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderInstance = sliderDebugElement.injector.get(MdSlider);
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
+ });
+
+ it('works in inverted mode', () => {
+ testComponent.invert = true;
+ fixture.detectChanges();
+
+ dispatchClickEventSequence(sliderNativeElement, 0.3);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(70);
+ });
+
+ it('works in RTL languages', () => {
+ testComponent.dir = 'rtl';
+ fixture.detectChanges();
+
+ dispatchClickEventSequence(sliderNativeElement, 0.3);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(70);
+ });
+
+ it('works in RTL languages in inverted mode', () => {
+ testComponent.dir = 'rtl';
+ testComponent.invert = true;
+ fixture.detectChanges();
+
+ dispatchClickEventSequence(sliderNativeElement, 0.3);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(30);
+ });
+
+ it('should decrement 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);
+ });
+
+ it('should increment inverted slider by 1 on left arrow pressed', () => {
+ testComponent.invert = true;
+ fixture.detectChanges();
+
+ dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(1);
+ });
+
+ it('should decrement RTL slider by 1 on right arrow pressed', () => {
+ testComponent.dir = 'rtl';
+ sliderInstance.value = 100;
+ fixture.detectChanges();
+
+ dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(99);
+ });
+
+ it('should increment RTL slider by 1 on left arrow pressed', () => {
+ testComponent.dir = 'rtl';
+ fixture.detectChanges();
+
+ dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
+ fixture.detectChanges();
+
+ 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', () => {
+ testComponent.dir = 'rtl';
+ testComponent.invert = true;
+ sliderInstance.value = 100;
+ fixture.detectChanges();
+
+ dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(99);
+ });
+ });
});
// Disable animations and make the slider an even 100px (+ 8px padding on either side)
@@ -934,6 +1053,15 @@ class SliderWithChangeHandler {
onChange() { }
}
+@Component({
+ template: `
`,
+ styles: [styles],
+})
+class SliderWithDirAndInvert {
+ dir = 'ltr';
+ invert = false;
+}
+
/**
* Dispatches a click event sequence (consisting of moueseenter, click) from an element.
* Note: The mouse event truncates the position for the click.
diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts
index e1713ea92d8d..63913fe7beff 100644
--- a/src/lib/slider/slider.ts
+++ b/src/lib/slider/slider.ts
@@ -7,12 +7,15 @@ import {
Output,
ViewEncapsulation,
forwardRef,
- EventEmitter
+ EventEmitter,
+ Optional
} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core';
import {Input as HammerInput} from 'hammerjs';
+import {Dir} from '../core/rtl/dir';
+import {CommonModule} from '@angular/common';
import {
PAGE_UP,
PAGE_DOWN,
@@ -67,6 +70,7 @@ 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-sliding]': '_isSliding',
'[class.md-slider-thumb-label-showing]': 'thumbLabel',
},
@@ -189,25 +193,47 @@ export class MdSlider implements ControlValueAccessor {
this._percent = this._calculatePercentage(this.value);
}
- get trackFillFlexBasis() {
- return this.percent * 100 + '%';
+ /** Whether the slider is inverted. */
+ @Input()
+ get invert() { return this._invert; }
+ set invert(value: boolean) { this._invert = coerceBooleanProperty(value); }
+ private _invert = false;
+
+ /** CSS styles for the track fill element. */
+ get trackFillStyles(): { [key: string]: string } {
+ return {
+ 'flexBasis': `${this.percent * 100}%`
+ };
}
- get ticksMarginLeft() {
- return this.tickIntervalPercent / 2 * 100 + '%';
+ /** CSS styles for the ticks container element. */
+ get ticksContainerStyles(): { [key: string]: string } {
+ return {
+ 'marginLeft': `${this.direction == 'rtl' ? '' : '-'}${this.tickIntervalPercent / 2 * 100}%`
+ };
}
- get ticksContainerMarginLeft() {
- return '-' + this.ticksMarginLeft;
+ /** CSS styles for the ticks element. */
+ get ticksStyles() {
+ let styles: { [key: string]: string } = {
+ 'backgroundSize': `${this.tickIntervalPercent * 100}% 2px`
+ };
+ if (this.direction == 'rtl') {
+ styles['marginRight'] = `-${this.tickIntervalPercent / 2 * 100}%`;
+ } else {
+ styles['marginLeft'] = `${this.tickIntervalPercent / 2 * 100}%`;
+ }
+ return styles;
}
- get ticksBackgroundSize() {
- return this.tickIntervalPercent * 100 + '% 2px';
+ /** The language direction for this slider element. */
+ get direction() {
+ return (this._dir && this._dir.value == 'rtl') ? 'rtl' : 'ltr';
}
@Output() change = new EventEmitter();
- constructor(elementRef: ElementRef) {
+ constructor(@Optional() private _dir: Dir, elementRef: ElementRef) {
this._renderer = new SliderRenderer(elementRef);
}
@@ -283,13 +309,13 @@ export class MdSlider implements ControlValueAccessor {
this.value = this.min;
break;
case LEFT_ARROW:
- this._increment(-1);
+ this._increment(this._isLeftMin() ? -1 : 1);
break;
case UP_ARROW:
this._increment(1);
break;
case RIGHT_ARROW:
- this._increment(1);
+ this._increment(this._isLeftMin() ? 1 : -1);
break;
case DOWN_ARROW:
this._increment(-1);
@@ -303,6 +329,11 @@ 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);
@@ -321,6 +352,9 @@ export class MdSlider implements ControlValueAccessor {
// 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()) {
+ percent = 1 - percent;
+ }
let exactValue = this._calculateValue(percent);
// This calculation finds the closest step by finding the closest whole number divisible by the
@@ -441,7 +475,7 @@ export class SliderRenderer {
@NgModule({
- imports: [FormsModule],
+ imports: [CommonModule, FormsModule],
exports: [MdSlider],
declarations: [MdSlider],
providers: [