Skip to content

Commit 13b7dd0

Browse files
mmalerbajelbourn
authored andcommitted
feat(slider): keyboard support (#1759)
1 parent f6944e4 commit 13b7dd0

File tree

5 files changed

+184
-18
lines changed

5 files changed

+184
-18
lines changed

src/demo-app/slider/slider-demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ <h1>Slider with two-way binding</h1>
3838
<md-tab label="One">
3939
<md-slider min="1" max="5" value="3"></md-slider>
4040
</md-tab>
41-
</md-tab-group>
41+
</md-tab-group>

src/lib/core/keyboard/keycodes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ export const DOWN_ARROW = 40;
99
export const RIGHT_ARROW = 39;
1010
export const LEFT_ARROW = 37;
1111

12+
export const PAGE_UP = 33;
13+
export const PAGE_DOWN = 34;
14+
15+
export const HOME = 36;
16+
export const END = 35;
17+
1218
export const ENTER = 13;
1319
export const SPACE = 32;
1420
export const TAB = 9;

src/lib/slider/slider.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
<span class="md-slider-thumb-label-text">{{value}}</span>
1111
</div>
1212
</div>
13-
</div>
13+
</div>

src/lib/slider/slider.spec.ts

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
22
import {ReactiveFormsModule, FormControl} from '@angular/forms';
33
import {Component, DebugElement} from '@angular/core';
4-
import {By} from '@angular/platform-browser';
4+
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
55
import {MdSlider, MdSliderModule} from './slider';
6-
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
76
import {TestGestureConfig} from './test-gesture-config';
7+
import {
8+
UP_ARROW,
9+
RIGHT_ARROW,
10+
DOWN_ARROW,
11+
PAGE_DOWN,
12+
PAGE_UP,
13+
END,
14+
HOME, LEFT_ARROW
15+
} from '../core/keyboard/keycodes';
816

917

1018
describe('MdSlider', () => {
@@ -746,6 +754,90 @@ describe('MdSlider', () => {
746754
expect(testComponent.onChange).toHaveBeenCalledTimes(1);
747755
});
748756
});
757+
758+
describe('keyboard support', () => {
759+
let fixture: ComponentFixture<StandardSlider>;
760+
let sliderDebugElement: DebugElement;
761+
let sliderNativeElement: HTMLElement;
762+
let sliderTrackElement: HTMLElement;
763+
let testComponent: StandardSlider;
764+
let sliderInstance: MdSlider;
765+
766+
beforeEach(() => {
767+
fixture = TestBed.createComponent(StandardSlider);
768+
fixture.detectChanges();
769+
770+
testComponent = fixture.debugElement.componentInstance;
771+
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
772+
sliderNativeElement = sliderDebugElement.nativeElement;
773+
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
774+
sliderInstance = sliderDebugElement.injector.get(MdSlider);
775+
});
776+
777+
it('should increment slider by 1 on up arrow pressed', () => {
778+
dispatchKeydownEvent(sliderNativeElement, UP_ARROW);
779+
fixture.detectChanges();
780+
781+
expect(sliderInstance.value).toBe(1);
782+
});
783+
784+
it('should increment slider by 1 on right arrow pressed', () => {
785+
dispatchKeydownEvent(sliderNativeElement, RIGHT_ARROW);
786+
fixture.detectChanges();
787+
788+
expect(sliderInstance.value).toBe(1);
789+
});
790+
791+
it('should decrement slider by 1 on down arrow pressed', () => {
792+
sliderInstance.value = 100;
793+
794+
dispatchKeydownEvent(sliderNativeElement, DOWN_ARROW);
795+
fixture.detectChanges();
796+
797+
expect(sliderInstance.value).toBe(99);
798+
});
799+
800+
it('should decrement slider by 1 on left arrow pressed', () => {
801+
sliderInstance.value = 100;
802+
803+
dispatchKeydownEvent(sliderNativeElement, LEFT_ARROW);
804+
fixture.detectChanges();
805+
806+
expect(sliderInstance.value).toBe(99);
807+
});
808+
809+
it('should increment slider by 10 on page up pressed', () => {
810+
dispatchKeydownEvent(sliderNativeElement, PAGE_UP);
811+
fixture.detectChanges();
812+
813+
expect(sliderInstance.value).toBe(10);
814+
});
815+
816+
it('should decrement slider by 10 on page down pressed', () => {
817+
sliderInstance.value = 100;
818+
819+
dispatchKeydownEvent(sliderNativeElement, PAGE_DOWN);
820+
fixture.detectChanges();
821+
822+
expect(sliderInstance.value).toBe(90);
823+
});
824+
825+
it('should set slider to max on end pressed', () => {
826+
dispatchKeydownEvent(sliderNativeElement, END);
827+
fixture.detectChanges();
828+
829+
expect(sliderInstance.value).toBe(100);
830+
});
831+
832+
it('should set slider to min on home pressed', () => {
833+
sliderInstance.value = 100;
834+
835+
dispatchKeydownEvent(sliderNativeElement, HOME);
836+
fixture.detectChanges();
837+
838+
expect(sliderInstance.value).toBe(0);
839+
});
840+
});
749841
});
750842

751843
// Disable animations and make the slider an even 100px (+ 8px padding on either side)
@@ -843,7 +935,7 @@ class SliderWithChangeHandler {
843935
}
844936

845937
/**
846-
* Dispatches a click event from an element.
938+
* Dispatches a click event sequence (consisting of moueseenter, click) from an element.
847939
* Note: The mouse event truncates the position for the click.
848940
* @param sliderElement The md-slider element from which the event will be dispatched.
849941
* @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,
9091001
let dimensions = trackElement.getBoundingClientRect();
9101002
let x = dimensions.left + (dimensions.width * percent);
9111003

1004+
dispatchMouseenterEvent(sliderElement);
1005+
9121006
gestureConfig.emitEventForElement('slidestart', sliderElement, {
9131007
center: { x: x },
9141008
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
@@ -936,10 +1030,7 @@ function dispatchSlideEndEvent(sliderElement: HTMLElement, percent: number,
9361030
/**
9371031
* Dispatches a mouseenter event from an element.
9381032
* Note: The mouse event truncates the position for the click.
939-
* @param trackElement The track element from which the event location will be calculated.
940-
* @param containerElement The container element from which the event will be dispatched.
941-
* @param percentage The percentage of the slider where the click should occur. Used to find the
942-
* physical location of the click.
1033+
* @param element The element from which the event will be dispatched.
9431034
*/
9441035
function dispatchMouseenterEvent(element: HTMLElement): void {
9451036
let dimensions = element.getBoundingClientRect();
@@ -951,3 +1042,18 @@ function dispatchMouseenterEvent(element: HTMLElement): void {
9511042
'mouseenter', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null);
9521043
element.dispatchEvent(event);
9531044
}
1045+
1046+
/**
1047+
* Dispatches a keydown event from an element.
1048+
* @param element The element from which the event will be dispatched.
1049+
* @param keyCode The key code of the key being pressed.
1050+
*/
1051+
function dispatchKeydownEvent(element: HTMLElement, keyCode: number): void {
1052+
let event: any = document.createEvent('KeyboardEvent');
1053+
(event.initKeyEvent || event.initKeyboardEvent).bind(event)(
1054+
'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode);
1055+
Object.defineProperty(event, 'keyCode', {
1056+
get: function() { return keyCode; }
1057+
});
1058+
element.dispatchEvent(event);
1059+
}

src/lib/slider/slider.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import {
2-
NgModule,
3-
ModuleWithProviders,
4-
Component,
5-
ElementRef,
6-
Input,
7-
Output,
8-
ViewEncapsulation,
9-
forwardRef,
10-
EventEmitter,
2+
NgModule,
3+
ModuleWithProviders,
4+
Component,
5+
ElementRef,
6+
Input,
7+
Output,
8+
ViewEncapsulation,
9+
forwardRef,
10+
EventEmitter
1111
} from '@angular/core';
1212
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
1313
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
1414
import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core';
1515
import {Input as HammerInput} from 'hammerjs';
16+
import {
17+
PAGE_UP,
18+
PAGE_DOWN,
19+
END,
20+
HOME,
21+
LEFT_ARROW,
22+
UP_ARROW,
23+
RIGHT_ARROW,
24+
DOWN_ARROW
25+
} from '../core/keyboard/keycodes';
1626

1727
/**
1828
* Visually, a 30px separation between tick marks looks best. This is very subjective but it is
@@ -43,10 +53,12 @@ export class MdSliderChange {
4353
host: {
4454
'(blur)': '_onBlur()',
4555
'(click)': '_onClick($event)',
56+
'(keydown)': '_onKeydown($event)',
4657
'(mouseenter)': '_onMouseenter()',
4758
'(slide)': '_onSlide($event)',
4859
'(slideend)': '_onSlideEnd()',
4960
'(slidestart)': '_onSlideStart($event)',
61+
'role': 'slider',
5062
'tabindex': '0',
5163
'[attr.aria-disabled]': 'disabled',
5264
'[attr.aria-valuemax]': 'max',
@@ -254,6 +266,48 @@ export class MdSlider implements ControlValueAccessor {
254266
this.onTouched();
255267
}
256268

269+
_onKeydown(event: KeyboardEvent) {
270+
if (this.disabled) { return; }
271+
272+
switch (event.keyCode) {
273+
case PAGE_UP:
274+
this._increment(10);
275+
break;
276+
case PAGE_DOWN:
277+
this._increment(-10);
278+
break;
279+
case END:
280+
this.value = this.max;
281+
break;
282+
case HOME:
283+
this.value = this.min;
284+
break;
285+
case LEFT_ARROW:
286+
this._increment(-1);
287+
break;
288+
case UP_ARROW:
289+
this._increment(1);
290+
break;
291+
case RIGHT_ARROW:
292+
this._increment(1);
293+
break;
294+
case DOWN_ARROW:
295+
this._increment(-1);
296+
break;
297+
default:
298+
// Return if the key is not one that we explicitly handle to avoid calling preventDefault on
299+
// it.
300+
return;
301+
}
302+
303+
event.preventDefault();
304+
}
305+
306+
/** Increments the slider by the given number of steps (negative number decrements). */
307+
private _increment(numSteps: number) {
308+
this.value = this._clamp(this.value + this.step * numSteps, this.min, this.max);
309+
}
310+
257311
/**
258312
* Calculate the new value from the new physical location. The value will always be snapped.
259313
*/

0 commit comments

Comments
 (0)