Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(slider): keyboard support #1759

Merged
merged 11 commits into from
Nov 11, 2016
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;
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);
}
70 changes: 61 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,46 @@ 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;
}

event.preventDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only preventDefault when it's one of the keys we care about? E.g., pressing spacebar should probably still scroll the page.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default:
  return;

:p

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that. Could you add a comment at the return like

// Return if the key is one that we don't specifically handle so we
// don't call preventDefault on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

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