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. */