From 1aa20ac79b9fada08851110b958cbd2cafc6779a Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 31 Oct 2016 15:54:15 -0700 Subject: [PATCH 01/11] fix(slider): refactor the slider to use percent values for the track fill and thumb position. --- src/demo-app/slider/slider-demo.html | 2 +- src/lib/slider/slider.html | 2 +- src/lib/slider/slider.spec.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) 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/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..298e2ee0095b 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -843,7 +843,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 +909,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') } From 1371889b1abb388d85bb3a1abfc47fe8e50cc6d2 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 3 Nov 2016 15:07:15 -0700 Subject: [PATCH 02/11] Addressed comments. --- src/lib/slider/slider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index ac7f0c6912bc..ca799a6a30e2 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -13,6 +13,7 @@ import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/for import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core'; import {Input as HammerInput} from 'hammerjs'; +import {CommonModule} from '@angular/common'; /** * Visually, a 30px separation between tick marks looks best. This is very subjective but it is @@ -126,6 +127,7 @@ export class MdSlider implements ControlValueAccessor { private _tickIntervalPercent: number = 0; get tickIntervalPercent() { return this._tickIntervalPercent; } + get halfTickIntervalPercent() { return this._tickIntervalPercent / 2; } /** The percentage of the slider that coincides with the value. */ private _percent: number = 0; @@ -387,7 +389,7 @@ export class SliderRenderer { @NgModule({ - imports: [FormsModule], + imports: [FormsModule, CommonModule], exports: [MdSlider], declarations: [MdSlider], providers: [ From c121a849350033ec5d35393a00581aced8fdc4bf Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 3 Nov 2016 16:21:29 -0700 Subject: [PATCH 03/11] PercentPipe was adding extra space before '%', so replaced it. --- src/lib/slider/slider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index ca799a6a30e2..61ca492e1806 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -127,7 +127,6 @@ export class MdSlider implements ControlValueAccessor { private _tickIntervalPercent: number = 0; get tickIntervalPercent() { return this._tickIntervalPercent; } - get halfTickIntervalPercent() { return this._tickIntervalPercent / 2; } /** The percentage of the slider that coincides with the value. */ private _percent: number = 0; From 6c336335e2ab29c2606ad878aaab1127e18f9847 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 4 Nov 2016 15:29:58 -0700 Subject: [PATCH 04/11] remove CommonModule from imports. --- src/lib/slider/slider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index 61ca492e1806..ac7f0c6912bc 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -13,7 +13,6 @@ import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/for import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; import {MdGestureConfig, coerceBooleanProperty, coerceNumberProperty} from '../core'; import {Input as HammerInput} from 'hammerjs'; -import {CommonModule} from '@angular/common'; /** * Visually, a 30px separation between tick marks looks best. This is very subjective but it is @@ -388,7 +387,7 @@ export class SliderRenderer { @NgModule({ - imports: [FormsModule, CommonModule], + imports: [FormsModule], exports: [MdSlider], declarations: [MdSlider], providers: [ From d45794587a1d2aee814a94dcc6ed4ad585dad5af Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 4 Nov 2016 16:25:29 -0700 Subject: [PATCH 05/11] fix(slider): keyboard support. --- src/lib/slider/slider.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index ac7f0c6912bc..52b1487aeba0 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -43,10 +43,19 @@ export class MdSliderChange { host: { '(blur)': '_onBlur()', '(click)': '_onClick($event)', + '(keydown.arrowdown)': '_increment($event, -1)', + '(keydown.arrowleft)': '_increment($event, -1)', + '(keydown.arrowright)': '_increment($event, 1)', + '(keydown.arrowup)': '_increment($event, 1)', + '(keydown.end)': '_onEndKeyPressed($event)', + '(keydown.home)': '_onHomeKeyPressed($event)', + '(keydown.pagedown)': '_increment($event, -10)', + '(keydown.pageup)': '_increment($event, 10)', '(mouseenter)': '_onMouseenter()', '(slide)': '_onSlide($event)', '(slideend)': '_onSlideEnd()', '(slidestart)': '_onSlideStart($event)', + 'role': 'slider', 'tabindex': '0', '[attr.aria-disabled]': 'disabled', '[attr.aria-valuemax]': 'max', @@ -254,6 +263,24 @@ export class MdSlider implements ControlValueAccessor { this.onTouched(); } + /** Increments the slider by the given number of steps (negative number decrements. */ + _increment(event: KeyboardEvent, numSteps: number) { + this.value = this._clamp(this.value + this.step * numSteps, this.min, this.max); + event.preventDefault(); + } + + /** Handles end key pressed. */ + _onEndKeyPressed(event: KeyboardEvent) { + this.value = this.max; + event.preventDefault(); + } + + /** Handles home key pressed. */ + _onHomeKeyPressed(event: KeyboardEvent) { + this.value = this.min; + event.preventDefault(); + } + /** * Calculate the new value from the new physical location. The value will always be snapped. */ From 7009719108943ade628cd2a00bfd4062ca0d7af6 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 7 Nov 2016 11:54:57 -0800 Subject: [PATCH 06/11] prevent keyboard interaction with disabled slider. --- src/lib/slider/slider.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index 52b1487aeba0..ae886e9b55d9 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -265,18 +265,24 @@ export class MdSlider implements ControlValueAccessor { /** Increments the slider by the given number of steps (negative number decrements. */ _increment(event: KeyboardEvent, numSteps: number) { + if (this.disabled) { return; } + this.value = this._clamp(this.value + this.step * numSteps, this.min, this.max); event.preventDefault(); } /** Handles end key pressed. */ _onEndKeyPressed(event: KeyboardEvent) { + if (this.disabled) { return; } + this.value = this.max; event.preventDefault(); } /** Handles home key pressed. */ _onHomeKeyPressed(event: KeyboardEvent) { + if (this.disabled) { return; } + this.value = this.min; event.preventDefault(); } From d7cafeb5acdc76865c0bf09ce64461b354a356c3 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 10 Nov 2016 13:34:34 -0800 Subject: [PATCH 07/11] fix comment --- src/lib/slider/slider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index ae886e9b55d9..b14aa627621e 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -263,7 +263,7 @@ export class MdSlider implements ControlValueAccessor { this.onTouched(); } - /** Increments the slider by the given number of steps (negative number decrements. */ + /** Increments the slider by the given number of steps (negative number decrements). */ _increment(event: KeyboardEvent, numSteps: number) { if (this.disabled) { return; } From 165e5acee7d7c8cd60f649f251dcf8a35ba15bbf Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 10 Nov 2016 15:01:56 -0800 Subject: [PATCH 08/11] switch to event.keyCode --- src/lib/slider/slider.ts | 57 +++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index b14aa627621e..af4a3b196b64 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -43,14 +43,7 @@ export class MdSliderChange { host: { '(blur)': '_onBlur()', '(click)': '_onClick($event)', - '(keydown.arrowdown)': '_increment($event, -1)', - '(keydown.arrowleft)': '_increment($event, -1)', - '(keydown.arrowright)': '_increment($event, 1)', - '(keydown.arrowup)': '_increment($event, 1)', - '(keydown.end)': '_onEndKeyPressed($event)', - '(keydown.home)': '_onHomeKeyPressed($event)', - '(keydown.pagedown)': '_increment($event, -10)', - '(keydown.pageup)': '_increment($event, 10)', + '(keydown)': '_onKeydown($event)', '(mouseenter)': '_onMouseenter()', '(slide)': '_onSlide($event)', '(slideend)': '_onSlideEnd()', @@ -263,28 +256,44 @@ export class MdSlider implements ControlValueAccessor { this.onTouched(); } - /** Increments the slider by the given number of steps (negative number decrements). */ - _increment(event: KeyboardEvent, numSteps: number) { + _onKeydown(event: KeyboardEvent) { if (this.disabled) { return; } - this.value = this._clamp(this.value + this.step * numSteps, this.min, this.max); - event.preventDefault(); - } - - /** Handles end key pressed. */ - _onEndKeyPressed(event: KeyboardEvent) { - if (this.disabled) { return; } + switch (event.keyCode) { + case 33: /* page up */ + this._increment(10); + break; + case 34: /* page down */ + this._increment(-10); + break; + case 35: /* end */ + this.value = this.max; + break; + case 36: /* home */ + this.value = this.min; + break; + case 37: /* left arrow */ + this._increment(-1); + break; + case 38: /* up arrow */ + this._increment(1); + break; + case 39: /* right arrow */ + this._increment(1); + break; + case 40: /* down arrow */ + this._increment(-1); + break; + default: + return; + } - this.value = this.max; event.preventDefault(); } - /** Handles home key pressed. */ - _onHomeKeyPressed(event: KeyboardEvent) { - if (this.disabled) { return; } - - this.value = this.min; - 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); } /** From 3378c27dd59e98b1f436c43923a1e639ad3ad898 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 10 Nov 2016 15:55:22 -0800 Subject: [PATCH 09/11] added tests --- src/lib/core/keyboard/keycodes.ts | 6 ++ src/lib/slider/slider.spec.ts | 102 +++++++++++++++++++++++++++++- src/lib/slider/slider.ts | 44 ++++++++----- 3 files changed, 133 insertions(+), 19 deletions(-) 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.spec.ts b/src/lib/slider/slider.spec.ts index 298e2ee0095b..b8629af77cdd 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) @@ -953,3 +1045,9 @@ function dispatchMouseenterEvent(element: HTMLElement): void { 'mouseenter', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null); element.dispatchEvent(event); } + +function dispatchKeydownEvent(element: HTMLElement, keyCode: number): void { + let event = new Event('keydown'); + (event).keyCode = keyCode; + element.dispatchEvent(event); +} diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index af4a3b196b64..2f2ae2a8adca 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 @@ -260,28 +270,28 @@ export class MdSlider implements ControlValueAccessor { if (this.disabled) { return; } switch (event.keyCode) { - case 33: /* page up */ + case PAGE_UP: this._increment(10); break; - case 34: /* page down */ + case PAGE_DOWN: this._increment(-10); break; - case 35: /* end */ + case END: this.value = this.max; break; - case 36: /* home */ + case HOME: this.value = this.min; break; - case 37: /* left arrow */ + case LEFT_ARROW: this._increment(-1); break; - case 38: /* up arrow */ + case UP_ARROW: this._increment(1); break; - case 39: /* right arrow */ + case RIGHT_ARROW: this._increment(1); break; - case 40: /* down arrow */ + case DOWN_ARROW: this._increment(-1); break; default: From 72fce3f77428a80a31239fab43b578a5154c01ac Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 11 Nov 2016 08:48:57 -0800 Subject: [PATCH 10/11] x-browserify keydown event dispatch --- src/lib/slider/slider.spec.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts index b8629af77cdd..ef635cd58e56 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -1030,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(); @@ -1046,8 +1043,17 @@ function dispatchMouseenterEvent(element: HTMLElement): void { 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 = new Event('keydown'); - (event).keyCode = keyCode; + 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); } From cf0e4ca04d26fdb4e9172a732651a9bee684683c Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 11 Nov 2016 09:29:44 -0800 Subject: [PATCH 11/11] comment why default: return; --- src/lib/slider/slider.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index 2f2ae2a8adca..e1713ea92d8d 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -295,6 +295,8 @@ export class MdSlider implements ControlValueAccessor { this._increment(-1); break; default: + // Return if the key is not one that we explicitly handle to avoid calling preventDefault on + // it. return; }