diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html
index 16a5816548a5..fc848033cec9 100644
--- a/src/demo-app/slider/slider-demo.html
+++ b/src/demo-app/slider/slider-demo.html
@@ -38,4 +38,4 @@
Slider with two-way binding
-
\ No newline at end of file
+
diff --git a/src/lib/core/keyboard/keycodes.ts b/src/lib/core/keyboard/keycodes.ts
index f3626d258539..6204987a0e8a 100644
--- a/src/lib/core/keyboard/keycodes.ts
+++ b/src/lib/core/keyboard/keycodes.ts
@@ -9,6 +9,12 @@ export const DOWN_ARROW = 40;
export const RIGHT_ARROW = 39;
export const LEFT_ARROW = 37;
+export const PAGE_UP = 33;
+export const PAGE_DOWN = 34;
+
+export const HOME = 36;
+export const END = 35;
+
export const ENTER = 13;
export const SPACE = 32;
export const TAB = 9;
diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html
index 538a371e3c78..688ea4bb348a 100644
--- a/src/lib/slider/slider.html
+++ b/src/lib/slider/slider.html
@@ -10,4 +10,4 @@
{{value}}
-
\ No newline at end of file
+
diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts
index 320b54a9e5b2..ef635cd58e56 100644
--- a/src/lib/slider/slider.spec.ts
+++ b/src/lib/slider/slider.spec.ts
@@ -1,10 +1,18 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ReactiveFormsModule, FormControl} from '@angular/forms';
import {Component, DebugElement} from '@angular/core';
-import {By} from '@angular/platform-browser';
+import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {MdSlider, MdSliderModule} from './slider';
-import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {TestGestureConfig} from './test-gesture-config';
+import {
+ UP_ARROW,
+ RIGHT_ARROW,
+ DOWN_ARROW,
+ PAGE_DOWN,
+ PAGE_UP,
+ END,
+ HOME, LEFT_ARROW
+} from '../core/keyboard/keycodes';
describe('MdSlider', () => {
@@ -746,6 +754,90 @@ describe('MdSlider', () => {
expect(testComponent.onChange).toHaveBeenCalledTimes(1);
});
});
+
+ describe('keyboard support', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderTrackElement: HTMLElement;
+ let testComponent: StandardSlider;
+ let sliderInstance: MdSlider;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StandardSlider);
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
+ sliderInstance = sliderDebugElement.injector.get(MdSlider);
+ });
+
+ it('should increment slider by 1 on up arrow pressed', () => {
+ dispatchKeydownEvent(sliderNativeElement, UP_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(1);
+ });
+
+ it('should increment slider by 1 on right arrow pressed', () => {
+ dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(1);
+ });
+
+ it('should decrement slider by 1 on down arrow pressed', () => {
+ sliderInstance.value = 100;
+
+ dispatchKeydownEvent(sliderNativeElement, DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(99);
+ });
+
+ it('should decrement slider by 1 on left arrow pressed', () => {
+ sliderInstance.value = 100;
+
+ dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(99);
+ });
+
+ it('should increment slider by 10 on page up pressed', () => {
+ dispatchKeydownEvent(sliderNativeElement, PAGE_UP);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(10);
+ });
+
+ it('should decrement slider by 10 on page down pressed', () => {
+ sliderInstance.value = 100;
+
+ dispatchKeydownEvent(sliderNativeElement, PAGE_DOWN);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(90);
+ });
+
+ it('should set slider to max on end pressed', () => {
+ dispatchKeydownEvent(sliderNativeElement, END);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(100);
+ });
+
+ it('should set slider to min on home pressed', () => {
+ sliderInstance.value = 100;
+
+ dispatchKeydownEvent(sliderNativeElement, HOME);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(0);
+ });
+ });
});
// Disable animations and make the slider an even 100px (+ 8px padding on either side)
@@ -843,7 +935,7 @@ class SliderWithChangeHandler {
}
/**
- * Dispatches a click event from an element.
+ * Dispatches a click event sequence (consisting of moueseenter, click) from an element.
* Note: The mouse event truncates the position for the click.
* @param sliderElement The md-slider element from which the event will be dispatched.
* @param percentage The percentage of the slider where the click should occur. Used to find the
@@ -909,6 +1001,8 @@ function dispatchSlideStartEvent(sliderElement: HTMLElement, percent: number,
let dimensions = trackElement.getBoundingClientRect();
let x = dimensions.left + (dimensions.width * percent);
+ dispatchMouseenterEvent(sliderElement);
+
gestureConfig.emitEventForElement('slidestart', sliderElement, {
center: { x: x },
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
@@ -936,10 +1030,7 @@ function dispatchSlideEndEvent(sliderElement: HTMLElement, percent: number,
/**
* Dispatches a mouseenter event from an element.
* Note: The mouse event truncates the position for the click.
- * @param trackElement The track element from which the event location will be calculated.
- * @param containerElement The container element from which the event will be dispatched.
- * @param percentage The percentage of the slider where the click should occur. Used to find the
- * physical location of the click.
+ * @param element The element from which the event will be dispatched.
*/
function dispatchMouseenterEvent(element: HTMLElement): void {
let dimensions = element.getBoundingClientRect();
@@ -951,3 +1042,18 @@ function dispatchMouseenterEvent(element: HTMLElement): void {
'mouseenter', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
+
+/**
+ * Dispatches a keydown event from an element.
+ * @param element The element from which the event will be dispatched.
+ * @param keyCode The key code of the key being pressed.
+ */
+function dispatchKeydownEvent(element: HTMLElement, keyCode: number): void {
+ let event: any = document.createEvent('KeyboardEvent');
+ (event.initKeyEvent || event.initKeyboardEvent).bind(event)(
+ 'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode);
+ Object.defineProperty(event, 'keyCode', {
+ get: function() { return keyCode; }
+ });
+ element.dispatchEvent(event);
+}
diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts
index ac7f0c6912bc..e1713ea92d8d 100644
--- a/src/lib/slider/slider.ts
+++ b/src/lib/slider/slider.ts
@@ -1,18 +1,28 @@
import {
- NgModule,
- ModuleWithProviders,
- Component,
- ElementRef,
- Input,
- Output,
- ViewEncapsulation,
- forwardRef,
- EventEmitter,
+ NgModule,
+ ModuleWithProviders,
+ Component,
+ ElementRef,
+ Input,
+ Output,
+ ViewEncapsulation,
+ forwardRef,
+ EventEmitter
} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core';
import {Input as HammerInput} from 'hammerjs';
+import {
+ PAGE_UP,
+ PAGE_DOWN,
+ END,
+ HOME,
+ LEFT_ARROW,
+ UP_ARROW,
+ RIGHT_ARROW,
+ DOWN_ARROW
+} from '../core/keyboard/keycodes';
/**
* Visually, a 30px separation between tick marks looks best. This is very subjective but it is
@@ -43,10 +53,12 @@ export class MdSliderChange {
host: {
'(blur)': '_onBlur()',
'(click)': '_onClick($event)',
+ '(keydown)': '_onKeydown($event)',
'(mouseenter)': '_onMouseenter()',
'(slide)': '_onSlide($event)',
'(slideend)': '_onSlideEnd()',
'(slidestart)': '_onSlideStart($event)',
+ 'role': 'slider',
'tabindex': '0',
'[attr.aria-disabled]': 'disabled',
'[attr.aria-valuemax]': 'max',
@@ -254,6 +266,48 @@ export class MdSlider implements ControlValueAccessor {
this.onTouched();
}
+ _onKeydown(event: KeyboardEvent) {
+ if (this.disabled) { return; }
+
+ switch (event.keyCode) {
+ case PAGE_UP:
+ this._increment(10);
+ break;
+ case PAGE_DOWN:
+ this._increment(-10);
+ break;
+ case END:
+ this.value = this.max;
+ break;
+ case HOME:
+ this.value = this.min;
+ break;
+ case LEFT_ARROW:
+ this._increment(-1);
+ break;
+ case UP_ARROW:
+ this._increment(1);
+ break;
+ case RIGHT_ARROW:
+ this._increment(1);
+ break;
+ case DOWN_ARROW:
+ this._increment(-1);
+ break;
+ default:
+ // Return if the key is not one that we explicitly handle to avoid calling preventDefault on
+ // it.
+ return;
+ }
+
+ event.preventDefault();
+ }
+
+ /** 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);
+ }
+
/**
* Calculate the new value from the new physical location. The value will always be snapped.
*/