Skip to content

Commit

Permalink
feat(slider): keyboard support (#1759)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba authored and jelbourn committed Nov 11, 2016
1 parent f6944e4 commit 13b7dd0
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/demo-app/slider/slider-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ <h1>Slider with two-way binding</h1>
<md-tab label="One">
<md-slider min="1" max="5" value="3"></md-slider>
</md-tab>
</md-tab-group>
</md-tab-group>
6 changes: 6 additions & 0 deletions src/lib/core/keyboard/keycodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/slider/slider.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
<span class="md-slider-thumb-label-text">{{value}}</span>
</div>
</div>
</div>
</div>
120 changes: 113 additions & 7 deletions src/lib/slider/slider.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -746,6 +754,90 @@ describe('MdSlider', () => {
expect(testComponent.onChange).toHaveBeenCalledTimes(1);
});
});

describe('keyboard support', () => {
let fixture: ComponentFixture<StandardSlider>;
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 = <HTMLElement>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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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') }
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
72 changes: 63 additions & 9 deletions src/lib/slider/slider.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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.
*/
Expand Down

0 comments on commit 13b7dd0

Please sign in to comment.