diff --git a/demo/src/app/components/datepicker/datepicker.module.ts b/demo/src/app/components/datepicker/datepicker.module.ts
index 67aab84c39..92911bd424 100644
--- a/demo/src/app/components/datepicker/datepicker.module.ts
+++ b/demo/src/app/components/datepicker/datepicker.module.ts
@@ -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',
@@ -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,
@@ -156,6 +164,7 @@ export const ROUTES = [
NgbdDatepickerRangeModule,
NgbdDatepickerRangePopupModule,
NgbdDatepickerAdapterModule,
+ NgbdDatepickerKeyboardModule,
...DEMO_CALENDAR_MODULES
],
declarations: [
diff --git a/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.html b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.html
new file mode 100644
index 0000000000..6a1b898173
--- /dev/null
+++ b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.html
@@ -0,0 +1,3 @@
+
This datepicker uses alt instead of shift keyboard shortcuts.
+
+
diff --git a/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.module.ts b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.module.ts
new file mode 100644
index 0000000000..34d43f8d3e
--- /dev/null
+++ b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.module.ts
@@ -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 {
+}
diff --git a/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.ts b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.ts
new file mode 100644
index 0000000000..f2b178e6b4
--- /dev/null
+++ b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.ts
@@ -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;
+}
diff --git a/demo/src/app/components/datepicker/overview/datepicker-overview.component.html b/demo/src/app/components/datepicker/overview/datepicker-overview.component.html
index 29335dd7c0..bdcea5df99 100644
--- a/demo/src/app/components/datepicker/overview/datepicker-overview.component.html
+++ b/demo/src/app/components/datepicker/overview/datepicker-overview.component.html
@@ -322,6 +322,12 @@ Input date parsing and formatting
+
+
+ You can customize keyboard navigation as in the custom keyboard
+ navigation example. The default keys are as follows:
+
+
diff --git a/src/datepicker/datepicker-integration.spec.ts b/src/datepicker/datepicker-integration.spec.ts
index 07768c7aa1..072323114a 100644
--- a/src/datepicker/datepicker-integration.spec.ts
+++ b/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', () => {
@@ -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;
+ let dp: NgbDatepicker;
+ let ngbCalendar: NgbCalendar;
+ let startDate: NgbDateStruct = new NgbDate(2018, 1, 1);
+
+ beforeEach(() => {
+ TestBed.overrideComponent(TestComponent, {
+ set: {
+ template: `
+ `,
+ 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);
+
+ spyOn(ngbCalendar, 'getNext');
+ });
+
+ it('should allow customize keyboard navigation', () => {
+ dp.onKeyDown({which: Key.PageUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}});
+ expect(ngbCalendar.getNext).toHaveBeenCalledWith(startDate, 'y', -1);
+ dp.onKeyDown({which: Key.PageUp, shiftKey: true, preventDefault: () => {}, stopPropagation: () => {}});
+ expect(ngbCalendar.getNext).toHaveBeenCalledWith(startDate, 'm', -1);
+ });
+
+ it('should allow access to default keyboard navigation', () => {
+ dp.onKeyDown({which: Key.ArrowUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}});
+ expect(ngbCalendar.getNext).toHaveBeenCalledWith(startDate, 'd', -7);
+ });
+ });
});
@Component({selector: 'test-cmp', template: ''})
diff --git a/src/datepicker/datepicker-keyboard-service.spec.ts b/src/datepicker/datepicker-keyboard-service.spec.ts
new file mode 100644
index 0000000000..90e519428f
--- /dev/null
+++ b/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) =>
+ ({which: keyCode, shiftKey: shift, preventDefault: () => {}, stopPropagation: () => {}});
+
+describe('ngb-datepicker-keyboard-service', () => {
+
+ let service: NgbDatepickerKeyboardService;
+ let calendar: NgbCalendar;
+ let mock: Partial;
+ 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);
+ 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();
+ });
+
+});
diff --git a/src/datepicker/datepicker-keyboard-service.ts b/src/datepicker/datepicker-keyboard-service.ts
new file mode 100644
index 0000000000..a35f22eecd
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/src/datepicker/datepicker-keymap-service.spec.ts b/src/datepicker/datepicker-keymap-service.spec.ts
deleted file mode 100644
index a3269ae813..0000000000
--- a/src/datepicker/datepicker-keymap-service.spec.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import {NgbDatepickerKeyMapService} from './datepicker-keymap-service';
-import {NgbCalendar, NgbCalendarGregorian} from './ngb-calendar';
-import {NgbDatepickerService} from './datepicker-service';
-import {TestBed} from '@angular/core/testing';
-import {Subject} from 'rxjs';
-import {NgbDate} from './ngb-date';
-import {Key} from '../util/key';
-import {Type} from '@angular/core';
-
-const event = (keyCode: number, shift = false) =>
- ({which: keyCode, shiftKey: shift, preventDefault: () => {}, stopPropagation: () => {}});
-
-describe('ngb-datepicker-keymap-service', () => {
-
- let service: NgbDatepickerKeyMapService;
- let calendar: NgbCalendar;
- let mock: {focus, focusMove, focusSelect, model$};
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [
- NgbDatepickerKeyMapService, {provide: NgbCalendar, useClass: NgbCalendarGregorian}, {
- provide: NgbDatepickerService,
- useValue: {focus: () => {}, focusMove: () => {}, focusSelect: () => {}, model$: new Subject()}
- }
- ]
- });
-
- calendar = TestBed.get(NgbCalendar as Type);
- service = TestBed.get(NgbDatepickerKeyMapService);
- mock = TestBed.get(NgbDatepickerService);
-
- spyOn(mock, 'focus');
- spyOn(mock, 'focusMove');
- spyOn(mock, 'focusSelect');
- });
-
- it('should be instantiated', () => { expect(service).toBeTruthy(); });
-
- it('should move focus by 1 day or 1 week with "Arrow" keys', () => {
- service.processKey(event(Key.ArrowUp));
- expect(mock.focusMove).toHaveBeenCalledWith('d', -7);
-
- service.processKey(event(Key.ArrowDown));
- expect(mock.focusMove).toHaveBeenCalledWith('d', 7);
-
- service.processKey(event(Key.ArrowLeft));
- expect(mock.focusMove).toHaveBeenCalledWith('d', -1);
-
- service.processKey(event(Key.ArrowRight));
- expect(mock.focusMove).toHaveBeenCalledWith('d', 1);
-
- expect(mock.focusMove).toHaveBeenCalledTimes(4);
- });
-
- it('should move focus by 1 month or year "PgUp" and "PageDown"', () => {
- service.processKey(event(Key.PageUp));
- expect(mock.focusMove).toHaveBeenCalledWith('m', -1);
-
- service.processKey(event(Key.PageDown));
- expect(mock.focusMove).toHaveBeenCalledWith('m', 1);
-
- service.processKey(event(Key.PageUp, true));
- expect(mock.focusMove).toHaveBeenCalledWith('y', -1);
-
- service.processKey(event(Key.PageDown, true));
- expect(mock.focusMove).toHaveBeenCalledWith('y', 1);
-
- expect(mock.focusMove).toHaveBeenCalledTimes(4);
- });
-
- it('should select focused date with "Space" and "Enter"', () => {
- service.processKey(event(Key.Enter));
- service.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"', () => {
- service.processKey(event(Key.Home));
- expect(mock.focus).toHaveBeenCalledWith(undefined);
-
- service.processKey(event(Key.End));
- expect(mock.focus).toHaveBeenCalledWith(undefined);
-
- mock.model$.next({firstDate: new NgbDate(2017, 1, 1), lastDate: new NgbDate(2017, 12, 1)});
-
- service.processKey(event(Key.Home));
- expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 1, 1));
-
- service.processKey(event(Key.End));
- expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 12, 1));
-
- expect(mock.focus).toHaveBeenCalledTimes(4);
- });
-
- it('should move focus to the "min" and "max" dates with "Home" and "End"', () => {
- service.processKey(event(Key.Home, true));
- expect(mock.focus).toHaveBeenCalledWith(undefined);
-
- service.processKey(event(Key.End, true));
- expect(mock.focus).toHaveBeenCalledWith(undefined);
-
- mock.model$.next({minDate: new NgbDate(2017, 1, 1), maxDate: new NgbDate(2017, 12, 1), months: []});
-
- service.processKey(event(Key.Home, true));
- expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 1, 1));
-
- service.processKey(event(Key.End, true));
- expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 12, 1));
-
- expect(mock.focus).toHaveBeenCalledTimes(4);
- });
-
- it('should prevent default and stop propagation of the known key', () => {
- let e = event(Key.ArrowUp);
- spyOn(e, 'preventDefault');
- spyOn(e, 'stopPropagation');
-
- service.processKey(e);
- expect(e.preventDefault).toHaveBeenCalled();
- expect(e.stopPropagation).toHaveBeenCalled();
-
- // unknown key
- e = event(23);
- spyOn(e, 'preventDefault');
- spyOn(e, 'stopPropagation');
-
- service.processKey(e);
- expect(e.preventDefault).not.toHaveBeenCalled();
- expect(e.stopPropagation).not.toHaveBeenCalled();
- });
-
-});
diff --git a/src/datepicker/datepicker-keymap-service.ts b/src/datepicker/datepicker-keymap-service.ts
deleted file mode 100644
index 0bf6769a25..0000000000
--- a/src/datepicker/datepicker-keymap-service.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import {Injectable} from '@angular/core';
-import {NgbDatepickerService} from './datepicker-service';
-import {NgbCalendar} from './ngb-calendar';
-import {Key} from '../util/key';
-import {NgbDate} from './ngb-date';
-
-@Injectable()
-export class NgbDatepickerKeyMapService {
- private _minDate: NgbDate;
- private _maxDate: NgbDate;
- private _firstViewDate: NgbDate;
- private _lastViewDate: NgbDate;
-
- constructor(private _service: NgbDatepickerService, private _calendar: NgbCalendar) {
- _service.model$.subscribe(model => {
- this._minDate = model.minDate;
- this._maxDate = model.maxDate;
- this._firstViewDate = model.firstDate;
- this._lastViewDate = model.lastDate;
- });
- }
-
- processKey(event: KeyboardEvent) {
- // tslint:disable-next-line:deprecation
- switch (event.which) {
- case Key.PageUp:
- this._service.focusMove(event.shiftKey ? 'y' : 'm', -1);
- break;
- case Key.PageDown:
- this._service.focusMove(event.shiftKey ? 'y' : 'm', 1);
- break;
- case Key.End:
- this._service.focus(event.shiftKey ? this._maxDate : this._lastViewDate);
- break;
- case Key.Home:
- this._service.focus(event.shiftKey ? this._minDate : this._firstViewDate);
- break;
- case Key.ArrowLeft:
- this._service.focusMove('d', -1);
- break;
- case Key.ArrowUp:
- this._service.focusMove('d', -this._calendar.getDaysPerWeek());
- break;
- case Key.ArrowRight:
- this._service.focusMove('d', 1);
- break;
- case Key.ArrowDown:
- this._service.focusMove('d', this._calendar.getDaysPerWeek());
- break;
- case Key.Enter:
- case Key.Space:
- this._service.focusSelect();
- break;
- default:
- return;
- }
-
- // note 'return' in default case
- event.preventDefault();
- event.stopPropagation();
- }
-}
diff --git a/src/datepicker/datepicker-service.spec.ts b/src/datepicker/datepicker-service.spec.ts
index 1a21f310a4..00a98d89cf 100644
--- a/src/datepicker/datepicker-service.spec.ts
+++ b/src/datepicker/datepicker-service.spec.ts
@@ -1,6 +1,6 @@
import {TestBed} from '@angular/core/testing';
import {NgbDatepickerService} from './datepicker-service';
-import {NgbCalendar, NgbCalendarGregorian} from './ngb-calendar';
+import {NgbCalendar, NgbCalendarGregorian, NgbPeriod} from './ngb-calendar';
import {NgbDate} from './ngb-date';
import {Subscription} from 'rxjs';
import {DatepickerViewModel} from './datepicker-view-model';
@@ -15,6 +15,7 @@ describe('ngb-datepicker-service', () => {
let mock: {onNext};
let selectDate: NgbDate;
let mockSelect: {onNext};
+ let focusMove: (NgbDate, NgbPeriod?, number?) => void;
let subscriptions: Subscription[];
@@ -35,6 +36,8 @@ describe('ngb-datepicker-service', () => {
subscriptions = [];
model = undefined;
selectDate = null;
+ focusMove = (focusDate: NgbDate, period?: NgbPeriod, number?: number) =>
+ service.focus(calendar.getNext(focusDate, period, number));
mock = {onNext: () => {}};
spyOn(mock, 'onNext');
@@ -306,12 +309,13 @@ describe('ngb-datepicker-service', () => {
it(`should change the tabindex when changing the current month`, () => {
service.displayMonths = 2;
- service.focus(new NgbDate(2018, 3, 31));
+ const date = new NgbDate(2018, 3, 31);
+ service.focus(date);
expect(getDay(5, 4, 0).tabindex).toEqual(0); // 31 march in the first month block
expect(getDay(5, 0, 1).tabindex).toEqual(-1); // 31 march in the second month block
- service.focusMove('d', 1);
+ focusMove(date, 'd', 1);
expect(getDay(5, 4, 0).tabindex).toEqual(-1); // 31 march in the first month block
expect(getDay(5, 0, 1).tabindex).toEqual(-1); // 31 march in the second month block
expect(getDay(6, 4, 0).tabindex).toEqual(-1); // 1st april in the first month block
@@ -321,12 +325,13 @@ describe('ngb-datepicker-service', () => {
it(`should set the aria-label when changing the current month`, () => {
service.displayMonths = 2;
- service.focus(new NgbDate(2018, 3, 31));
+ const date = new NgbDate(2018, 3, 31);
+ service.focus(date);
expect(getDay(5, 4, 0).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the first month block
expect(getDay(5, 0, 1).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the second month block
- service.focusMove('d', 1);
+ focusMove(date, 'd', 1);
expect(getDay(5, 4, 0).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the first month block
expect(getDay(5, 0, 1).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the second month block
expect(getDay(6, 4, 0).ariaLabel).toEqual('Sunday, April 1, 2018'); // 1st april in the first month block
@@ -364,7 +369,7 @@ describe('ngb-datepicker-service', () => {
expect(model.focusDate).toEqual(today);
// focusMove
- service.focusMove('d', 1); // nope
+ focusMove(today, 'd', 1); // nope
expect(model.focusDate).toEqual(today);
expect(mock.onNext).toHaveBeenCalledTimes(2);
@@ -1009,37 +1014,6 @@ describe('ngb-datepicker-service', () => {
expect(mock.onNext).toHaveBeenCalledTimes(3);
});
- it(`should move focus with 'focusMove()'`, () => {
- const date = new NgbDate(2017, 5, 5);
-
- // days
- service.focus(date);
- service.focusMove('d', 1);
- expect(model.focusDate).toEqual(new NgbDate(2017, 5, 6));
-
- service.focus(date);
- service.focusMove('d', -1);
- expect(model.focusDate).toEqual(new NgbDate(2017, 5, 4));
-
- // months
- service.focus(date);
- service.focusMove('m', 1);
- expect(model.focusDate).toEqual(new NgbDate(2017, 6, 5));
-
- service.focus(date);
- service.focusMove('m', -1);
- expect(model.focusDate).toEqual(new NgbDate(2017, 4, 5));
-
- // years
- service.focus(date);
- service.focusMove('y', 1);
- expect(model.focusDate).toEqual(new NgbDate(2018, 5, 5));
-
- service.focus(date);
- service.focusMove('y', -1);
- expect(model.focusDate).toEqual(new NgbDate(2016, 5, 5));
- });
-
it(`should move focus when 'minDate' changes`, () => {
service.focus(new NgbDate(2017, 5, 5));
service.maxDate = new NgbDate(2017, 5, 1);
@@ -1324,7 +1298,8 @@ describe('ngb-datepicker-service', () => {
it(`should update 'focused' flag and tabindex for day template`, () => {
// off
- service.focus(new NgbDate(2017, 5, 1));
+ const date = new NgbDate(2017, 5, 1);
+ service.focus(date);
expect(getDayCtx(0).focused).toBeFalsy();
expect(getDayCtx(1).focused).toBeFalsy();
expect(getDay(0).tabindex).toEqual(0);
@@ -1338,7 +1313,7 @@ describe('ngb-datepicker-service', () => {
expect(getDay(1).tabindex).toEqual(-1);
// move
- service.focusMove('d', 1);
+ focusMove(date, 'd', 1);
expect(getDayCtx(0).focused).toBeFalsy();
expect(getDayCtx(1).focused).toBeTruthy();
expect(getDay(0).tabindex).toEqual(-1);
diff --git a/src/datepicker/datepicker-service.ts b/src/datepicker/datepicker-service.ts
index 88b0665bfe..9aa948a370 100644
--- a/src/datepicker/datepicker-service.ts
+++ b/src/datepicker/datepicker-service.ts
@@ -117,10 +117,6 @@ export class NgbDatepickerService {
}
}
- focusMove(period?: NgbPeriod, number?: number) {
- this.focus(this._calendar.getNext(this._state.focusDate, period, number));
- }
-
focusSelect() {
if (isDateSelectable(this._state.focusDate, this._state)) {
this.select(this._state.focusDate, {emitEvent: true});
diff --git a/src/datepicker/datepicker.module.ts b/src/datepicker/datepicker.module.ts
index a4455b2d3e..850f9ee9b3 100644
--- a/src/datepicker/datepicker.module.ts
+++ b/src/datepicker/datepicker.module.ts
@@ -8,7 +8,7 @@ import {NgbInputDatepicker} from './datepicker-input';
import {NgbDatepickerDayView} from './datepicker-day-view';
import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select';
-export {NgbDatepicker, NgbDatepickerNavigateEvent} from './datepicker';
+export {NgbDatepicker, NgbDatepickerNavigateEvent, NgbDatepickerState} from './datepicker';
export {NgbInputDatepicker} from './datepicker-input';
export {NgbCalendar, NgbPeriod, NgbCalendarGregorian} from './ngb-calendar';
export {NgbCalendarIslamicCivil} from './hijri/ngb-calendar-islamic-civil';
@@ -28,6 +28,7 @@ export {NgbDateAdapter} from './adapters/ngb-date-adapter';
export {NgbDateNativeAdapter} from './adapters/ngb-date-native-adapter';
export {NgbDateNativeUTCAdapter} from './adapters/ngb-date-native-utc-adapter';
export {NgbDateParserFormatter} from './ngb-date-parser-formatter';
+export {NgbDatepickerKeyboardService} from './datepicker-keyboard-service';
@NgModule({
declarations: [
diff --git a/src/datepicker/datepicker.spec.ts b/src/datepicker/datepicker.spec.ts
index cde4bba145..30fcbd95cb 100644
--- a/src/datepicker/datepicker.spec.ts
+++ b/src/datepicker/datepicker.spec.ts
@@ -6,14 +6,16 @@ import {Component, TemplateRef, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {FormsModule, ReactiveFormsModule, FormGroup, FormControl, Validators} from '@angular/forms';
+import {NgbCalendar} from './ngb-calendar';
import {NgbDatepickerModule, NgbDatepickerNavigateEvent} from './datepicker.module';
import {NgbDate} from './ngb-date';
import {NgbDatepickerConfig} from './datepicker-config';
-import {NgbDatepicker} from './datepicker';
+import {NgbDatepicker, NgbDatepickerState} from './datepicker';
import {DayTemplateContext} from './datepicker-day-template-context';
import {NgbDateStruct} from './ngb-date-struct';
import {NgbDatepickerMonthView} from './datepicker-month-view';
import {NgbDatepickerDayView} from './datepicker-day-view';
+import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service';
import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select';
import {NgbDatepickerNavigation} from './datepicker-navigation';
@@ -1195,6 +1197,82 @@ describe('ngb-datepicker', () => {
expectSameValues(datepicker, config);
});
});
+
+ describe('NgbDatepicker', () => {
+
+ let mockState: NgbDatepickerState;
+ let dp: NgbDatepicker;
+ let keyboardService: NgbDatepickerKeyboardService;
+ const mockKeyboardService: NgbDatepickerKeyboardService = {
+ processKey(event: KeyboardEvent, datepicker: NgbDatepicker, calendar: NgbCalendar) {
+ mockState = datepicker.state;
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [NgbDatepickerModule],
+ providers: [{provide: NgbDatepickerKeyboardService, useValue: mockKeyboardService}]
+ });
+ const fixture = createTestComponent(
+ ``);
+ fixture.detectChanges();
+ keyboardService = TestBed.get(NgbDatepickerKeyboardService);
+ dp = fixture.debugElement.query(By.directive(NgbDatepicker)).componentInstance;
+ });
+
+ it('should provide an defensive copy of minDate', () => {
+ dp.onKeyDown({});
+ expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1}));
+ expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31}));
+ expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1}));
+ expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31}));
+ Object.assign(mockState, {minDate: undefined});
+ dp.onKeyDown({});
+ expect(dp.model.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1}));
+ });
+
+ it('should provide an defensive copy of maxDate', () => {
+ dp.onKeyDown({});
+ expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1}));
+ expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31}));
+ expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1}));
+ expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31}));
+ Object.assign(mockState, {maxDate: undefined});
+ dp.onKeyDown({});
+ expect(dp.model.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31}));
+ });
+
+ it('should provide an defensive copy of firstDate', () => {
+ dp.onKeyDown({});
+ expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1}));
+ expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31}));
+ expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1}));
+ expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31}));
+ Object.assign(mockState, {firstDate: undefined});
+ dp.onKeyDown({});
+ expect(dp.model.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1}));
+ });
+
+ it('should provide an defensive copy of lastDate', () => {
+ dp.onKeyDown({});
+ expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1}));
+ expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31}));
+ expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1}));
+ expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31}));
+ Object.assign(mockState, {lastDate: undefined});
+ dp.onKeyDown({});
+ expect(dp.model.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31}));
+ });
+
+ it('should provide an defensive copy of focusDate', () => {
+ dp.onKeyDown({});
+ expect(mockState.focusDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1}));
+ Object.assign(mockState, {focusDate: undefined});
+ dp.onKeyDown({});
+ expect(mockState.focusDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1}));
+ });
+ });
});
@Component({selector: 'test-cmp', template: ''})
diff --git a/src/datepicker/datepicker.ts b/src/datepicker/datepicker.ts
index a17e65854f..7463493442 100644
--- a/src/datepicker/datepicker.ts
+++ b/src/datepicker/datepicker.ts
@@ -23,7 +23,7 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {NgbCalendar} from './ngb-calendar';
import {NgbDate} from './ngb-date';
import {NgbDatepickerService} from './datepicker-service';
-import {NgbDatepickerKeyMapService} from './datepicker-keymap-service';
+import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service';
import {DatepickerViewModel, NavigationEvent} from './datepicker-view-model';
import {DayTemplateContext} from './datepicker-day-template-context';
import {NgbDatepickerConfig} from './datepicker-config';
@@ -61,6 +61,37 @@ export interface NgbDatepickerNavigateEvent {
preventDefault: () => void;
}
+/**
+ * An object that represents a part of the state of the datepicker.
+ * Required to override datepicker services i.e. the datepicker-keyboard-service.
+ */
+export interface NgbDatepickerState {
+ /**
+ * The minDate provided as input.
+ */
+ readonly minDate: NgbDate;
+
+ /**
+ * The maxDate provided as input.
+ */
+ readonly maxDate: NgbDate;
+
+ /**
+ * The first date of current month.
+ */
+ readonly firstDate: NgbDate;
+
+ /**
+ * The last date of current month.
+ */
+ readonly lastDate: NgbDate;
+
+ /**
+ * The focused date.
+ */
+ readonly focusDate?: NgbDate;
+}
+
/**
* A highly configurable component that helps you with selecting calendar dates.
*
@@ -117,11 +148,12 @@ export interface NgbDatepickerNavigateEvent {
`,
- providers: [NGB_DATEPICKER_VALUE_ACCESSOR, NgbDatepickerService, NgbDatepickerKeyMapService]
+ providers: [NGB_DATEPICKER_VALUE_ACCESSOR, NgbDatepickerService]
})
export class NgbDatepicker implements OnDestroy,
OnChanges, OnInit, AfterViewInit, ControlValueAccessor {
model: DatepickerViewModel;
+ publicState: NgbDatepickerState = Object.create({});
@ViewChild('months', {static: true}) private _monthsEl: ElementRef;
private _controlValue: NgbDate;
@@ -246,8 +278,8 @@ export class NgbDatepicker implements OnDestroy,
onTouched = () => {};
constructor(
- private _keyMapService: NgbDatepickerKeyMapService, public _service: NgbDatepickerService,
- private _calendar: NgbCalendar, public i18n: NgbDatepickerI18n, config: NgbDatepickerConfig,
+ public _service: NgbDatepickerService, private _calendar: NgbCalendar, public i18n: NgbDatepickerI18n,
+ config: NgbDatepickerConfig, private _keyboardService: NgbDatepickerKeyboardService,
private _cd: ChangeDetectorRef, private _elementRef: ElementRef,
private _ngbDateAdapter: NgbDateAdapter, private _ngZone: NgZone) {
['dayTemplate', 'dayTemplateData', 'displayMonths', 'firstDayOfWeek', 'footerTemplate', 'markDisabled', 'minDate',
@@ -298,6 +330,30 @@ export class NgbDatepicker implements OnDestroy,
});
}
+ /**
+ * Returns a copy of the state of the datepicker.
+ */
+ get state(): NgbDatepickerState {
+ Object.assign(this.publicState, {
+ maxDate: this.model.maxDate,
+ minDate: this.model.minDate,
+ firstDate: this.model.firstDate,
+ lastDate: this.model.lastDate,
+ focusDate: this.model.focusDate,
+ });
+ return this.publicState;
+ }
+
+ /**
+ * Focuses on given date.
+ */
+ focusDate(date: NgbDateStruct): void { this._service.focus(NgbDate.from(date)); }
+
+ /**
+ * Selects focused date.
+ */
+ focusSelect(): void { this._service.focusSelect(); }
+
focus() {
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
const elementToFocus =
@@ -367,7 +423,7 @@ export class NgbDatepicker implements OnDestroy,
this._service.select(date, {emitEvent: true});
}
- onKeyDown(event: KeyboardEvent) { this._keyMapService.processKey(event); }
+ onKeyDown(event: KeyboardEvent) { this._keyboardService.processKey(event, this, this._calendar); }
onNavigateDateSelect(date: NgbDate) { this._service.open(date); }
diff --git a/src/index.ts b/src/index.ts
index a1d7c7a71b..ddc59a1f97 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -60,8 +60,10 @@ export {
NgbDatepickerConfig,
NgbDatepickerI18n,
NgbDatepickerI18nHebrew,
+ NgbDatepickerKeyboardService,
NgbDatepickerModule,
NgbDatepickerNavigateEvent,
+ NgbDatepickerState,
NgbDateStruct,
NgbInputDatepicker,
NgbPeriod