diff --git a/src/components/slider/slider.html b/src/components/slider/slider.html
new file mode 100644
index 00000000000..e53edc4cb68
--- /dev/null
+++ b/src/components/slider/slider.html
@@ -0,0 +1,15 @@
+
diff --git a/src/components/slider/slider.scss b/src/components/slider/slider.scss
new file mode 100644
index 00000000000..f9bd5fad869
--- /dev/null
+++ b/src/components/slider/slider.scss
@@ -0,0 +1,143 @@
+@import 'default-theme';
+@import '_variables';
+
+// This refers to the thickness of the slider. On a horizontal slider this is the height, on a
+// vertical slider this is the width.
+$md-slider-thickness: 20px !default;
+$md-slider-min-size: 128px !default;
+$md-slider-padding: 8px !default;
+
+$md-slider-track-height: 2px !default;
+$md-slider-thumb-size: 20px !default;
+
+$md-slider-thumb-default-scale: 0.7 !default;
+$md-slider-thumb-focus-scale: 1 !default;
+
+// TODO(iveysaur): Find an implementation to hide the track under a disabled thumb.
+$md-slider-off-color: rgba(black, 0.26);
+$md-slider-focused-color: rgba(black, 0.38);
+$md-slider-disabled-color: rgba(black, 0.26);
+
+/**
+ * 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);
+}
+
+/**
+ * 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);
+ width: $width;
+ height: $height;
+ border-radius: max($width, $height);
+}
+
+md-slider {
+ height: $md-slider-thickness;
+ min-width: $md-slider-min-size;
+ position: relative;
+ padding: 0;
+ display: inline-block;
+ outline: none;
+ vertical-align: middle;
+}
+
+md-slider *,
+md-slider *::after {
+ box-sizing: border-box;
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Holds the isActive and isDragging classes as well as helps with positioning the children.
+ * Cannot be merged with .md-slider-wrapper.
+ */
+.md-slider-container {
+ position: relative;
+}
+
+.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-track {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 100%;
+ background-color: $md-slider-off-color;
+}
+
+.md-slider-track-fill {
+ transition-duration: $swift-ease-out-duration;
+ transition-timing-function: $swift-ease-out-timing-function;
+ transition-property: width, height;
+ background-color: md-color($md-accent);
+}
+
+.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;
+}
+
+.md-slider-thumb {
+ z-index: 1;
+
+ @include slider-thumb-position($md-slider-thumb-size, $md-slider-thumb-size);
+ 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);
+ border-width: 3px;
+ border-style: solid;
+ transition: inherit;
+ background-color: md-color($md-accent);
+ border-color: md-color($md-accent);
+}
+
+.md-slider-dragging .md-slider-thumb-position,
+.md-slider-dragging .md-slider-track-fill {
+ transition: none;
+ cursor: default;
+}
+
+.md-slider-active .md-slider-thumb {
+ transform: scale($md-slider-thumb-focus-scale);
+}
+
+.md-slider-disabled .md-slider-thumb::after {
+ background-color: $md-slider-disabled-color;
+ border-color: $md-slider-disabled-color;
+}
diff --git a/src/components/slider/slider.spec.ts b/src/components/slider/slider.spec.ts
new file mode 100644
index 00000000000..1278914fba5
--- /dev/null
+++ b/src/components/slider/slider.spec.ts
@@ -0,0 +1,425 @@
+import {
+ addProviders,
+ inject,
+ async,
+} from '@angular/core/testing';
+import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
+import {Component, DebugElement, ViewEncapsulation} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {MdSlider, MD_SLIDER_DIRECTIVES} from './slider';
+import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
+import {TestGestureConfig} from './test-gesture-config';
+
+describe('MdSlider', () => {
+ let builder: TestComponentBuilder;
+ let gestureConfig: TestGestureConfig;
+
+ beforeEach(() => {
+ addProviders([
+ {provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
+ gestureConfig = new TestGestureConfig();
+ return gestureConfig;
+ }}
+ ]);
+ });
+
+ beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
+ builder = tcb;
+ }));
+
+ describe('standard slider', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderInstance: MdSlider;
+ let trackFillElement: HTMLElement;
+ let trackFillDimensions: ClientRect;
+ let sliderTrackElement: HTMLElement;
+ let sliderDimensions: ClientRect;
+ let thumbElement: HTMLElement;
+ let thumbDimensions: ClientRect;
+ let thumbWidth: number;
+
+ beforeEach(async(() => {
+ builder.createAsync(StandardSlider).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ 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();
+ thumbWidth =
+ sliderNativeElement.querySelector('.md-slider-thumb').getBoundingClientRect().width;
+ });
+ }));
+
+ it('should set the default values', () => {
+ expect(sliderInstance.value).toBe(0);
+ expect(sliderInstance.min).toBe(0);
+ expect(sliderInstance.max).toBe(100);
+ });
+
+ it('should update the value on a click', () => {
+ expect(sliderInstance.value).toBe(0);
+ dispatchClickEvent(sliderTrackElement, 0.19);
+ // The expected value is 19 from: percentage * difference of max and min.
+ let difference = Math.abs(sliderInstance.value - 19);
+ expect(difference).toBeLessThan(1);
+ });
+
+ it('should update the value on a drag', () => {
+ expect(sliderInstance.value).toBe(0);
+ dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.89, gestureConfig);
+ // The expected value is 89 from: percentage * difference of max and min.
+ let difference = Math.abs(sliderInstance.value - 89);
+ expect(difference).toBeLessThan(1);
+ });
+
+ it('should set the value as min when dragging before the track', () => {
+ expect(sliderInstance.value).toBe(0);
+ dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, -1.33, gestureConfig);
+ expect(sliderInstance.value).toBe(0);
+ });
+
+ it('should set the value as max when dragging past the track', () => {
+ expect(sliderInstance.value).toBe(0);
+ dispatchDragEvent(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(sliderTrackElement, 0.39);
+
+ trackFillDimensions = trackFillElement.getBoundingClientRect();
+ // The fill should be close to the slider's width * the percentage from the click.
+ let difference = Math.abs(trackFillDimensions.width - (sliderDimensions.width * 0.39));
+ expect(difference).toBeLessThan(1);
+ });
+
+ it('should update the thumb position on click', () => {
+ expect(thumbDimensions.left).toBe(sliderDimensions.left - (thumbWidth / 2));
+ dispatchClickEvent(sliderTrackElement, 0.16);
+
+ thumbDimensions = thumbElement.getBoundingClientRect();
+ // The thumb's offset is expected to be equal to the slider's offset + 0.16 * the slider's
+ // width - half the thumb width (to center the thumb).
+ let offset = sliderDimensions.left + (sliderDimensions.width * 0.16) - (thumbWidth / 2);
+ let difference = Math.abs(thumbDimensions.left - offset);
+ expect(difference).toBeLessThan(1);
+ });
+
+ it('should update the track fill on drag', () => {
+ expect(trackFillDimensions.width).toBe(0);
+ dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.86, gestureConfig);
+
+ trackFillDimensions = trackFillElement.getBoundingClientRect();
+ let difference = Math.abs(trackFillDimensions.width - (sliderDimensions.width * 0.86));
+ expect(difference).toBeLessThan(1);
+ });
+
+ it('should update the thumb position on drag', () => {
+ expect(thumbDimensions.left).toBe(sliderDimensions.left - (thumbWidth / 2));
+ dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.27, gestureConfig);
+
+ thumbDimensions = thumbElement.getBoundingClientRect();
+ let offset = sliderDimensions.left + (sliderDimensions.width * 0.27) - (thumbWidth / 2);
+ let difference = Math.abs(thumbDimensions.left - offset);
+ expect(difference).toBeLessThan(1);
+ });
+
+ 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');
+
+ dispatchClickEvent(sliderNativeElement, 0.23);
+ fixture.detectChanges();
+
+ expect(containerElement.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);
+ fixture.detectChanges();
+
+ expect(containerElement.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');
+ });
+
+ it('should add and remove the md-slider-dragging class when dragging', () => {
+ let containerElement = sliderNativeElement.querySelector('.md-slider-container');
+ expect(containerElement.classList).not.toContain('md-slider-dragging');
+
+ dispatchDragStartEvent(sliderNativeElement, 0, gestureConfig);
+ fixture.detectChanges();
+
+ expect(containerElement.classList).toContain('md-slider-dragging');
+
+ dispatchDragEndEvent(sliderNativeElement, 0.34, gestureConfig);
+ fixture.detectChanges();
+
+ expect(containerElement.classList).not.toContain('md-slider-dragging');
+ });
+ });
+
+ describe('disabled slider', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderInstance: MdSlider;
+
+ beforeEach(async(() => {
+ builder.createAsync(DisabledSlider).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderInstance = sliderDebugElement.componentInstance;
+ });
+ }));
+
+ it('should be disabled', () => {
+ expect(sliderInstance.disabled).toBeTruthy();
+ });
+
+ it('should not change the value on click when disabled', () => {
+ expect(sliderInstance.value).toBe(0);
+ dispatchClickEvent(sliderNativeElement, 0.63);
+ expect(sliderInstance.value).toBe(0);
+ });
+
+ it('should not change the value on drag when disabled', () => {
+ expect(sliderInstance.value).toBe(0);
+ dispatchDragEvent(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');
+
+ dispatchClickEvent(sliderNativeElement, 0.43);
+ fixture.detectChanges();
+
+ expect(containerElement.classList).not.toContain('md-slider-active');
+ });
+
+ it('should not add the md-slider-dragging class on drag when disabled', () => {
+ let containerElement = sliderNativeElement.querySelector('.md-slider-container');
+ expect(containerElement.classList).not.toContain('md-slider-dragging');
+
+ dispatchDragStartEvent(sliderNativeElement, 0.46, gestureConfig);
+ fixture.detectChanges();
+
+ expect(containerElement.classList).not.toContain('md-slider-dragging');
+ });
+ });
+
+ describe('slider with set min and max', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderInstance: MdSlider;
+ let sliderTrackElement: HTMLElement;
+
+ beforeEach(async(() => {
+ builder.createAsync(SliderWithMinAndMax).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderInstance = sliderDebugElement.injector.get(MdSlider);
+ sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
+ });
+ }));
+
+ it('should set the default values from the attributes', () => {
+ expect(sliderInstance.value).toBe(5);
+ expect(sliderInstance.min).toBe(5);
+ expect(sliderInstance.max).toBe(15);
+ });
+
+ it('should set the correct value on click', () => {
+ dispatchClickEvent(sliderTrackElement, 0.09);
+ // 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 = 5 + (0.09 * (15 - 5));
+ let difference = Math.abs(sliderInstance.value - value);
+ expect(difference).toBeLessThan(1);
+ });
+
+ it('should set the correct value on drag', () => {
+ dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.62, gestureConfig);
+ // 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 = 5 + (0.62 * (15 - 5));
+ let difference = Math.abs(sliderInstance.value - value);
+ expect(difference).toBeLessThan(1);
+ });
+ });
+
+ describe('slider with set value', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderInstance: MdSlider;
+ let sliderTrackElement: HTMLElement;
+
+ beforeEach(async(() => {
+ builder.createAsync(SliderWithValue).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderInstance = sliderDebugElement.injector.get(MdSlider);
+ sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
+ });
+ }));
+
+ it('should set the default value from the attribute', () => {
+ expect(sliderInstance.value).toBe(26);
+ });
+
+ it('should set the correct value on click', () => {
+ dispatchClickEvent(sliderTrackElement, 0.92);
+ // 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.
+ let value = 92;
+ let difference = Math.abs(sliderInstance.value - value);
+ expect(difference).toBeLessThan(1);
+ });
+
+ it('should set the correct value on drag', () => {
+ dispatchDragEvent(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig);
+ expect(sliderInstance.value).toBe(32);
+ });
+ });
+});
+
+// The transition has to be removed in order to test the updated positions without setTimeout.
+@Component({
+ directives: [MD_SLIDER_DIRECTIVES],
+ template: ``,
+ styles: [`
+ .md-slider-track-fill, .md-slider-thumb-position {
+ transition: none !important;
+ }
+ `],
+ encapsulation: ViewEncapsulation.None
+})
+class StandardSlider { }
+
+@Component({
+ directives: [MD_SLIDER_DIRECTIVES],
+ template: ``
+})
+class DisabledSlider { }
+
+@Component({
+ directives: [MD_SLIDER_DIRECTIVES],
+ template: ``
+})
+class SliderWithMinAndMax { }
+
+@Component({
+ directives: [MD_SLIDER_DIRECTIVES],
+ template: ``
+})
+class SliderWithValue { }
+
+/**
+ * Dispatches a click event from an element.
+ * @param element The 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(element: HTMLElement, percentage: number): void {
+ let dimensions = element.getBoundingClientRect();
+ let y = dimensions.top;
+ let x = dimensions.left + (dimensions.width * percentage);
+
+ let event = document.createEvent('MouseEvent');
+ event.initMouseEvent(
+ 'click', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null);
+ element.dispatchEvent(event);
+}
+
+/**
+ * Dispatches a drag 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 startPercent The percentage of the slider where the drag will begin.
+ * @param endPercent The percentage of the slider where the drag will end.
+ * @param gestureConfig The gesture config for the test to handle emitting the drag events.
+ */
+function dispatchDragEvent(trackElement: HTMLElement, containerElement: HTMLElement,
+ startPercent: number, endPercent: number,
+ gestureConfig: TestGestureConfig): void {
+ let dimensions = trackElement.getBoundingClientRect();
+ let startX = dimensions.left + (dimensions.width * startPercent);
+ let endX = dimensions.left + (dimensions.width * endPercent);
+
+ gestureConfig.emitEventForElement('dragstart', containerElement, {
+ // The actual event has a center with an x value that the drag listener is looking for.
+ center: { x: startX },
+ // The event needs a source event with a prevent default so we fake one.
+ srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
+ });
+
+ gestureConfig.emitEventForElement('drag', containerElement, {
+ center: { x: endX },
+ srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
+ });
+}
+
+/**
+ * Dispatches a dragstart event from an element.
+ * @param element The element from which the event will be dispatched.
+ * @param startPercent The percentage of the slider where the drag will begin.
+ * @param gestureConfig The gesture config for the test to handle emitting the drag events.
+ */
+function dispatchDragStartEvent(element: HTMLElement, startPercent: number,
+ gestureConfig: TestGestureConfig): void {
+ let dimensions = element.getBoundingClientRect();
+ let x = dimensions.left + (dimensions.width * startPercent);
+
+ gestureConfig.emitEventForElement('dragstart', element, {
+ center: { x: x },
+ srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
+ });
+}
+
+/**
+ * Dispatches a dragend event from an element.
+ * @param element The element from which the event will be dispatched.
+ * @param endPercent The percentage of the slider where the drag will end.
+ * @param gestureConfig The gesture config for the test to handle emitting the drag events.
+ */
+function dispatchDragEndEvent(element: HTMLElement, endPercent: number,
+ gestureConfig: TestGestureConfig): void {
+ let dimensions = element.getBoundingClientRect();
+ let x = dimensions.left + (dimensions.width * endPercent);
+
+ gestureConfig.emitEventForElement('dragend', element, {
+ center: { x: x },
+ srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
+ });
+}
diff --git a/src/components/slider/slider.ts b/src/components/slider/slider.ts
new file mode 100644
index 00000000000..78d5e8a2b1c
--- /dev/null
+++ b/src/components/slider/slider.ts
@@ -0,0 +1,253 @@
+import {
+ Component,
+ ElementRef,
+ HostBinding,
+ Input,
+ ViewEncapsulation,
+ AfterContentInit,
+} from '@angular/core';
+import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
+import {applyCssTransform} from '@angular2-material/core/style/apply-transform';
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-slider',
+ host: {
+ 'tabindex': '0',
+ '(click)': 'onClick($event)',
+ '(drag)': 'onDrag($event)',
+ '(dragstart)': 'onDragStart($event)',
+ '(dragend)': 'onDragEnd()',
+ '(window:resize)': 'onResize()',
+ '(blur)': 'onBlur()',
+ },
+ templateUrl: 'slider.html',
+ styleUrls: ['slider.css'],
+ encapsulation: ViewEncapsulation.None,
+})
+export class MdSlider implements AfterContentInit {
+ /** 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;
+
+ @Input()
+ @BooleanFieldValue()
+ @HostBinding('class.md-slider-disabled')
+ @HostBinding('attr.aria-disabled')
+ disabled: boolean = false;
+
+ /** 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;
+
+ /**
+ * Whether or not the thumb is currently being dragged.
+ * Used to determine if there should be a transition for the thumb and fill track.
+ * TODO: internal
+ */
+ isDragging: boolean = false;
+
+ /**
+ * Whether or not the slider is active (clicked or is being dragged).
+ * Used to shrink and grow the thumb as according to the Material Design spec.
+ * TODO: internal
+ */
+ 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;
+
+ @Input()
+ @HostBinding('attr.aria-valuemin')
+ get min() {
+ return this._min;
+ }
+
+ set min(v: number) {
+ // This has to be forced as a number to handle the math later.
+ this._min = Number(v);
+
+ // If the value wasn't explicitly set by the user, set it to the min.
+ if (!this._isInitialized) {
+ this.value = this._min;
+ }
+ }
+
+ @Input()
+ @HostBinding('attr.aria-valuemax')
+ get max() {
+ return this._max;
+ }
+
+ set max(v: number) {
+ this._max = Number(v);
+ }
+
+ @Input()
+ @HostBinding('attr.aria-valuenow')
+ get value() {
+ return this._value;
+ }
+
+ set value(v: number) {
+ this._value = Number(v);
+ this._isInitialized = true;
+ this.updatePercentFromValue();
+ }
+
+ constructor(elementRef: ElementRef) {
+ 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() {
+ this._sliderDimensions = this._renderer.getSliderDimensions();
+ this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
+ }
+
+ /** TODO: internal */
+ onClick(event: MouseEvent) {
+ if (this.disabled) {
+ return;
+ }
+
+ this.isActive = true;
+ this.isDragging = false;
+ this._renderer.addFocus();
+ this.updateValueFromPosition(event.clientX);
+ }
+
+ /** TODO: internal */
+ onDrag(event: HammerInput) {
+ if (this.disabled) {
+ return;
+ }
+ // Prevent the drag from selecting anything else.
+ event.preventDefault();
+ this.updateValueFromPosition(event.center.x);
+ }
+
+ /** TODO: internal */
+ onDragStart(event: HammerInput) {
+ if (this.disabled) {
+ return;
+ }
+
+ event.preventDefault();
+ this.isDragging = true;
+ this.isActive = true;
+ this._renderer.addFocus();
+ this.updateValueFromPosition(event.center.x);
+ }
+
+ /** TODO: internal */
+ onDragEnd() {
+ this.isDragging = false;
+ }
+
+ /** TODO: internal */
+ onResize() {
+ this.isDragging = true;
+ this._sliderDimensions = this._renderer.getSliderDimensions();
+ // Skip updating the value and position as there is no new placement.
+ this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
+ }
+
+ /** TODO: internal */
+ onBlur() {
+ this.isActive = false;
+ }
+
+ /**
+ * When the value changes without a physical position, the percentage needs to be recalculated
+ * independent of the physical location.
+ */
+ updatePercentFromValue() {
+ this._percent = (this.value - this.min) / (this.max - this.min);
+ }
+
+ /**
+ * Calculate the new value from the new physical location.
+ */
+ updateValueFromPosition(pos: number) {
+ let offset = this._sliderDimensions.left;
+ let size = this._sliderDimensions.width;
+ this._percent = this.clamp((pos - offset) / size);
+ this.value = this.min + (this._percent * (this.max - this.min));
+
+ this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
+ }
+
+ /**
+ * Return a number between two numbers.
+ */
+ clamp(value: number, min = 0, max = 1) {
+ return Math.max(min, Math.min(value, max));
+ }
+}
+
+/**
+ * Renderer class in order to keep all dom manipulation in one place and outside of the main class.
+ */
+export class SliderRenderer {
+ private _sliderElement: HTMLElement;
+
+ constructor(elementRef: ElementRef) {
+ this._sliderElement = elementRef.nativeElement;
+ }
+
+ /**
+ * Get the bounding client rect of the slider track element.
+ * The track is used rather than the native element to ignore the extra space that the thumb can
+ * take up.
+ */
+ getSliderDimensions() {
+ let trackElement = this._sliderElement.querySelector('.md-slider-track');
+ return trackElement.getBoundingClientRect();
+ }
+
+ /**
+ * Update the physical position of the thumb and fill track on the slider.
+ */
+ updateThumbAndFillPosition(percent: number, width: number) {
+ // The actual thumb element. Needed to get the exact width of the thumb for calculations.
+ let thumbElement = this._sliderElement.querySelector('.md-slider-thumb');
+ // 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 thumbWidth = thumbElement.getBoundingClientRect().width;
+
+ let position = percent * width;
+ // The thumb needs to be shifted to the left by half of the width of itself so that it centers
+ // on the value.
+ let thumbPosition = position - (thumbWidth / 2);
+
+ fillTrackElement.style.width = `${position}px`;
+ applyCssTransform(thumbPositionElement, `translateX(${thumbPosition}px)`);
+ }
+
+ /**
+ * Focuses the native element.
+ * Currently only used to allow a blur event to fire but will be used with keyboard input later.
+ */
+ addFocus() {
+ this._sliderElement.focus();
+ }
+}
+
+export const MD_SLIDER_DIRECTIVES = [MdSlider];
diff --git a/src/components/slider/test-gesture-config.ts b/src/components/slider/test-gesture-config.ts
new file mode 100644
index 00000000000..dc308d22242
--- /dev/null
+++ b/src/components/slider/test-gesture-config.ts
@@ -0,0 +1,39 @@
+import {Injectable} from '@angular/core';
+import {MdGestureConfig} from '@angular2-material/core/gestures/MdGestureConfig';
+
+/**
+ * An extension of MdGestureConfig that exposes the underlying HammerManager instances.
+ * Tests can use these instances to emit fake gesture events.
+ */
+@Injectable()
+export class TestGestureConfig extends MdGestureConfig {
+ /**
+ * A map of Hammer instances to element.
+ * Used to emit events over instances for an element.
+ */
+ hammerInstances: Map = new Map();
+
+ /**
+ * Create a mapping of Hammer instances to element so that events can be emitted during testing.
+ */
+ buildHammer(element: HTMLElement) {
+ let mc = super.buildHammer(element);
+
+ if (this.hammerInstances.get(element)) {
+ this.hammerInstances.get(element).push(mc);
+ } else {
+ this.hammerInstances.set(element, [mc]);
+ }
+
+ return mc;
+ }
+
+ /**
+ * The Angular event plugin for Hammer creates a new HammerManager instance for each listener,
+ * so we need to apply our event on all instances to hit the correct listener.
+ */
+ emitEventForElement(eventType: string, element: HTMLElement, eventData: Object) {
+ let instances = this.hammerInstances.get(element);
+ instances.forEach(instance => instance.emit(eventType, eventData));
+ }
+}
diff --git a/src/demo-app/demo-app/demo-app.html b/src/demo-app/demo-app/demo-app.html
index b4a050770ee..34595a6fd69 100644
--- a/src/demo-app/demo-app/demo-app.html
+++ b/src/demo-app/demo-app/demo-app.html
@@ -18,6 +18,7 @@
Progress Bar
Radio
Sidenav
+ Slider
Slide Toggle
Toolbar
Tabs
diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts
index 3b3dc228415..d54146c6e49 100644
--- a/src/demo-app/demo-app/routes.ts
+++ b/src/demo-app/demo-app/routes.ts
@@ -17,6 +17,7 @@ import {PortalDemo} from '../portal/portal-demo';
import {ProgressBarDemo} from '../progress-bar/progress-bar-demo';
import {ProgressCircleDemo} from '../progress-circle/progress-circle-demo';
import {SlideToggleDemo} from '../slide-toggle/slide-toggle-demo';
+import {SliderDemo} from '../slider/slider-demo';
import {SidenavDemo} from '../sidenav/sidenav-demo';
import {RadioDemo} from '../radio/radio-demo';
import {CardDemo} from '../card/card-demo';
@@ -31,6 +32,7 @@ export const routes: RouterConfig = [
{path: 'radio', component: RadioDemo},
{path: 'sidenav', component: SidenavDemo},
{path: 'slide-toggle', component: SlideToggleDemo},
+ {path: 'slider', component: SliderDemo},
{path: 'progress-circle', component: ProgressCircleDemo},
{path: 'progress-bar', component: ProgressBarDemo},
{path: 'portal', component: PortalDemo},
diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html
new file mode 100644
index 00000000000..dd3716c75d8
--- /dev/null
+++ b/src/demo-app/slider/slider-demo.html
@@ -0,0 +1,22 @@
+Default Slider
+
+ Label
+ {{slidey.value}}
+
+
+Slider with Min and Max
+
+
+Disabled Slider
+
+
+Slider with set value
+
diff --git a/src/demo-app/slider/slider-demo.ts b/src/demo-app/slider/slider-demo.ts
new file mode 100644
index 00000000000..ec77e356b61
--- /dev/null
+++ b/src/demo-app/slider/slider-demo.ts
@@ -0,0 +1,10 @@
+import {Component} from '@angular/core';
+import {MD_SLIDER_DIRECTIVES} from '@angular2-material/slider/slider';
+
+@Component({
+ moduleId: module.id,
+ selector: 'slider-demo',
+ templateUrl: 'slider-demo.html',
+ directives: [MD_SLIDER_DIRECTIVES],
+})
+export class SliderDemo { }
diff --git a/src/demo-app/system-config.ts b/src/demo-app/system-config.ts
index bb7efe92848..9b61e634616 100644
--- a/src/demo-app/system-config.ts
+++ b/src/demo-app/system-config.ts
@@ -15,6 +15,7 @@ const components = [
'progress-circle',
'radio',
'sidenav',
+ 'slider',
'slide-toggle',
'button-toggle',
'tabs',
diff --git a/test/karma.config.ts b/test/karma.config.ts
index bf57e4a2a8e..e384d192ee0 100644
--- a/test/karma.config.ts
+++ b/test/karma.config.ts
@@ -25,6 +25,7 @@ export function config(config) {
{pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false},
{pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false},
{pattern: 'dist/vendor/zone.js/dist/fake-async-test.js', included: true, watched: false},
+ {pattern: 'dist/vendor/hammerjs/hammer.min.js', included: true, watched: false},
{pattern: 'test/karma-test-shim.js', included: true, watched: false},