Skip to content

Commit

Permalink
feat(datepicker): export NgbDatepickerKeyboardService
Browse files Browse the repository at this point in the history
  • Loading branch information
gpolychronis-amadeus authored and maxokorokov committed Nov 15, 2019
1 parent 1057dbd commit e5b3222
Show file tree
Hide file tree
Showing 16 changed files with 488 additions and 246 deletions.
9 changes: 9 additions & 0 deletions demo/src/app/components/datepicker/datepicker.module.ts
Expand Up @@ -35,6 +35,8 @@ import { NgbdDatepickerOverviewComponent } from './overview/datepicker-overview.
import { NgbdDatepickerOverviewDemoComponent } from './overview/demo/datepicker-overview-demo.component';
import { NgbdDatepickerPositiontargetModule } from './demos/positiontarget/datepicker-position-target.module';
import { NgbdDatepickerPositiontarget } from './demos/positiontarget/datepicker-positiontarget';
import { NgbdDatepickerKeyboard } from './demos/keyboard/datepicker-keyboard';
import { NgbdDatepickerKeyboardModule } from './demos/keyboard/datepicker-keyboard.module';

const OVERVIEW = {
'basic-usage': 'Basic Usage',
Expand Down Expand Up @@ -117,6 +119,12 @@ const DEMOS = {
code: require('!!raw-loader!./demos/positiontarget/datepicker-positiontarget').default,
markup: require('!!raw-loader!./demos/positiontarget/datepicker-positiontarget.html').default
},
keyboard: {
title: 'Custom keyboard navigation',
type: NgbdDatepickerKeyboard,
code: require('!!raw-loader!./demos/keyboard/datepicker-keyboard').default,
markup: require('!!raw-loader!./demos/keyboard/datepicker-keyboard.html').default
},
config: {
title: 'Global configuration of datepickers',
type: NgbdDatepickerConfig,
Expand Down Expand Up @@ -156,6 +164,7 @@ export const ROUTES = [
NgbdDatepickerRangeModule,
NgbdDatepickerRangePopupModule,
NgbdDatepickerAdapterModule,
NgbdDatepickerKeyboardModule,
...DEMO_CALENDAR_MODULES
],
declarations: [
Expand Down
@@ -0,0 +1,3 @@
<p>This datepicker uses alt instead of shift keyboard shortcuts.</p>

<ngb-datepicker [(ngModel)]="model"></ngb-datepicker>
@@ -0,0 +1,15 @@
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

import {NgbdDatepickerKeyboard} from './datepicker-keyboard';

@NgModule({
imports: [BrowserModule, FormsModule, NgbModule],
declarations: [NgbdDatepickerKeyboard],
exports: [NgbdDatepickerKeyboard],
bootstrap: [NgbdDatepickerKeyboard]
})
export class NgbdDatepickerKeyboardModule {
}
@@ -0,0 +1,49 @@
import {Component, Injectable} from '@angular/core';
import {
NgbCalendar,
NgbDatepicker,
NgbDatepickerKeyboardService,
NgbDateStruct
} from '@ng-bootstrap/ng-bootstrap';

const Key = {
PageUp: 'PageUp',
PageDown: 'PageDown',
End: 'End',
Home: 'Home'
};

@Injectable()
export class CustomKeyboardService extends NgbDatepickerKeyboardService {
processKey(event: KeyboardEvent, dp: NgbDatepicker, calendar: NgbCalendar) {
const state = dp.state;
switch (event.code) {
case Key.PageUp:
dp.focusDate(calendar.getNext(state.focusDate, event.altKey ? 'y' : 'm', -1));
break;
case Key.PageDown:
dp.focusDate(calendar.getNext(state.focusDate, event.altKey ? 'y' : 'm', 1));
break;
case Key.End:
dp.focusDate(event.altKey ? state.maxDate : state.lastDate);
break;
case Key.Home:
dp.focusDate(event.altKey ? state.minDate : state.firstDate);
break;
default:
super.processKey(event, dp, calendar);
return;
}
event.preventDefault();
event.stopPropagation();
}
}

@Component({
selector: 'ngbd-datepicker-keyboard',
templateUrl: './datepicker-keyboard.html',
providers: [{provide: NgbDatepickerKeyboardService, useClass: CustomKeyboardService}]
})
export class NgbdDatepickerKeyboard {
model: NgbDateStruct;
}
Expand Up @@ -322,6 +322,12 @@ <h4>Input date parsing and formatting</h4>

<!-- Keyboard -->
<ngbd-overview-section [section]="sections['keyboard-shortcuts']">

<p>
You can customize keyboard navigation as in the <a routerLink="../examples" fragment="keyboard">custom keyboard
navigation example</a>. The default keys are as follows:
</p>

<table class="table mt-4">
<tbody>
<tr>
Expand Down
65 changes: 64 additions & 1 deletion src/datepicker/datepicker-integration.spec.ts
@@ -1,10 +1,14 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, Injectable} from '@angular/core';
import {Component, Injectable, Type} from '@angular/core';
import {By} from '@angular/platform-browser';
import {NgbDatepickerModule, NgbDateStruct} from './datepicker.module';
import {NgbCalendar, NgbCalendarGregorian} from './ngb-calendar';
import {NgbDate} from './ngb-date';
import {getMonthSelect, getYearSelect} from '../test/datepicker/common';
import {NgbDatepickerI18n, NgbDatepickerI18nDefault} from './datepicker-i18n';
import {NgbDatepicker} from './datepicker';
import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service';
import {Key} from '../util/key';

describe('ngb-datepicker integration', () => {

Expand Down Expand Up @@ -107,6 +111,65 @@ describe('ngb-datepicker integration', () => {
expect(monthNames).toEqual(['A 8102', 'B 8102']);
});
});

describe('keyboard service', () => {

@Injectable()
class CustomKeyboardService extends NgbDatepickerKeyboardService {
processKey(event: KeyboardEvent, service: NgbDatepicker, calendar: NgbCalendar) {
const state = service.state;
// tslint:disable-next-line:deprecation
switch (event.which) {
case Key.PageUp:
service.focusDate(calendar.getNext(state.focusDate, event.altKey ? 'y' : 'm', -1));
break;
case Key.PageDown:
service.focusDate(calendar.getNext(state.focusDate, event.altKey ? 'y' : 'm', 1));
break;
default:
super.processKey(event, service, calendar);
return;
}
event.preventDefault();
event.stopPropagation();
}
}

let fixture: ComponentFixture<TestComponent>;
let dp: NgbDatepicker;
let ngbCalendar: NgbCalendar;
let startDate: NgbDateStruct = new NgbDate(2018, 1, 1);

beforeEach(() => {
TestBed.overrideComponent(TestComponent, {
set: {
template: `
<ngb-datepicker [startDate]="{year: 2018, month: 1}" [displayMonths]="1"></ngb-datepicker>`,
providers: [{provide: NgbDatepickerKeyboardService, useClass: CustomKeyboardService}]
}
});

fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker);
ngbCalendar = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbCalendar as Type<NgbCalendar>);

spyOn(ngbCalendar, 'getNext');
});

it('should allow customize keyboard navigation', () => {
dp.onKeyDown(<any>{which: Key.PageUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}});
expect(ngbCalendar.getNext).toHaveBeenCalledWith(startDate, 'y', -1);
dp.onKeyDown(<any>{which: Key.PageUp, shiftKey: true, preventDefault: () => {}, stopPropagation: () => {}});
expect(ngbCalendar.getNext).toHaveBeenCalledWith(startDate, 'm', -1);
});

it('should allow access to default keyboard navigation', () => {
dp.onKeyDown(<any>{which: Key.ArrowUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}});
expect(ngbCalendar.getNext).toHaveBeenCalledWith(startDate, 'd', -7);
});
});
});

@Component({selector: 'test-cmp', template: ''})
Expand Down
130 changes: 130 additions & 0 deletions src/datepicker/datepicker-keyboard-service.spec.ts
@@ -0,0 +1,130 @@
import {NgbDatepicker, NgbDatepickerState} from './datepicker';
import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service';
import {NgbCalendar, NgbCalendarGregorian} from './ngb-calendar';
import {TestBed} from '@angular/core/testing';
import {NgbDate} from './ngb-date';
import {Key} from '../util/key';
import {Type} from '@angular/core';

const event = (keyCode: number, shift = false) =>
<any>({which: keyCode, shiftKey: shift, preventDefault: () => {}, stopPropagation: () => {}});

describe('ngb-datepicker-keyboard-service', () => {

let service: NgbDatepickerKeyboardService;
let calendar: NgbCalendar;
let mock: Partial<NgbDatepicker>;
let processKey = function(e: KeyboardEvent) { service.processKey(e, mock as NgbDatepicker, calendar); };
let state: NgbDatepickerState = Object.assign({focusDate: {day: 1, month: 1, year: 2018}});

beforeEach(() => {
TestBed.configureTestingModule(
{providers: [{provide: NgbCalendar, useClass: NgbCalendarGregorian}, NgbDatepickerKeyboardService]});

calendar = TestBed.get(NgbCalendar as Type<NgbCalendar>);
service = TestBed.get(NgbDatepickerKeyboardService);
mock = {state, focusDate: () => {}, focusSelect: () => {}};

spyOn(mock, 'focusDate');
spyOn(mock, 'focusSelect');
spyOn(calendar, 'getNext');
});

it('should be instantiated', () => { expect(service).toBeTruthy(); });

it('should move focus by 1 day or 1 week with "Arrow" keys', () => {
processKey(event(Key.ArrowUp));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'd', -7);

processKey(event(Key.ArrowDown));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'd', 7);

processKey(event(Key.ArrowLeft));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'd', -1);

processKey(event(Key.ArrowRight));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'd', 1);

expect(calendar.getNext).toHaveBeenCalledTimes(4);
});

it('should move focus by 1 month or year "PgUp" and "PageDown"', () => {
processKey(event(Key.PageUp));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'm', -1);

processKey(event(Key.PageDown));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'm', 1);

processKey(event(Key.PageUp, true));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'y', -1);

processKey(event(Key.PageDown, true));
expect(calendar.getNext).toHaveBeenCalledWith(state.focusDate, 'y', 1);

expect(calendar.getNext).toHaveBeenCalledTimes(4);
});

it('should select focused date with "Space" and "Enter"', () => {
processKey(event(Key.Enter));
processKey(event(Key.Space));
expect(mock.focusSelect).toHaveBeenCalledTimes(2);
});

it('should move focus to the first and last days in the view with "Home" and "End"', () => {
processKey(event(Key.Home));
expect(mock.focusDate).toHaveBeenCalledWith(undefined);

processKey(event(Key.End));
expect(mock.focusDate).toHaveBeenCalledWith(undefined);

Object.assign(state, {firstDate: new NgbDate(2017, 1, 1)});
Object.assign(state, {lastDate: new NgbDate(2017, 12, 1)});

processKey(event(Key.Home));
expect(mock.focusDate).toHaveBeenCalledWith(new NgbDate(2017, 1, 1));

processKey(event(Key.End));
expect(mock.focusDate).toHaveBeenCalledWith(new NgbDate(2017, 12, 1));

expect(mock.focusDate).toHaveBeenCalledTimes(4);
});

it('should move focus to the "min" and "max" dates with "Home" and "End"', () => {
processKey(event(Key.Home, true));
expect(mock.focusDate).toHaveBeenCalledWith(undefined);

processKey(event(Key.End, true));
expect(mock.focusDate).toHaveBeenCalledWith(undefined);

Object.assign(state, {minDate: new NgbDate(2017, 1, 1)});
Object.assign(state, {maxDate: new NgbDate(2017, 12, 1)});

processKey(event(Key.Home, true));
expect(mock.focusDate).toHaveBeenCalledWith(new NgbDate(2017, 1, 1));

processKey(event(Key.End, true));
expect(mock.focusDate).toHaveBeenCalledWith(new NgbDate(2017, 12, 1));

expect(mock.focusDate).toHaveBeenCalledTimes(4);
});

it('should prevent default and stop propagation of the known key', () => {
let e = event(Key.ArrowUp);
spyOn(e, 'preventDefault');
spyOn(e, 'stopPropagation');

processKey(e);
expect(e.preventDefault).toHaveBeenCalled();
expect(e.stopPropagation).toHaveBeenCalled();

// unknown key
e = event(23);
spyOn(e, 'preventDefault');
spyOn(e, 'stopPropagation');

processKey(e);
expect(e.preventDefault).not.toHaveBeenCalled();
expect(e.stopPropagation).not.toHaveBeenCalled();
});

});
54 changes: 54 additions & 0 deletions src/datepicker/datepicker-keyboard-service.ts
@@ -0,0 +1,54 @@
import {Injectable} from '@angular/core';
import {NgbCalendar} from './ngb-calendar';
import {NgbDatepicker} from './datepicker';
import {Key} from '../util/key';

/**
* A service that represents the keyboard navigation.
*
* The default navigation is documented in the overview.
*/
@Injectable({providedIn: 'root'})
export class NgbDatepickerKeyboardService {
/**
* Processes a keyboard event.
*/
processKey(event: KeyboardEvent, datepicker: NgbDatepicker, calendar: NgbCalendar) {
const state = datepicker.state;
// tslint:disable-next-line:deprecation
switch (event.which) {
case Key.PageUp:
datepicker.focusDate(calendar.getNext(state.focusDate, event.shiftKey ? 'y' : 'm', -1));
break;
case Key.PageDown:
datepicker.focusDate(calendar.getNext(state.focusDate, event.shiftKey ? 'y' : 'm', 1));
break;
case Key.End:
datepicker.focusDate(event.shiftKey ? state.maxDate : state.lastDate);
break;
case Key.Home:
datepicker.focusDate(event.shiftKey ? state.minDate : state.firstDate);
break;
case Key.ArrowLeft:
datepicker.focusDate(calendar.getNext(state.focusDate, 'd', -1));
break;
case Key.ArrowUp:
datepicker.focusDate(calendar.getNext(state.focusDate, 'd', -calendar.getDaysPerWeek()));
break;
case Key.ArrowRight:
datepicker.focusDate(calendar.getNext(state.focusDate, 'd', 1));
break;
case Key.ArrowDown:
datepicker.focusDate(calendar.getNext(state.focusDate, 'd', calendar.getDaysPerWeek()));
break;
case Key.Enter:
case Key.Space:
datepicker.focusSelect();
break;
default:
return;
}
event.preventDefault();
event.stopPropagation();
}
}

0 comments on commit e5b3222

Please sign in to comment.