Skip to content

Commit

Permalink
feat(datepicker): esc can close datepicker (#3966)
Browse files Browse the repository at this point in the history
Fixes #3890
* tab can close datpicker, esc can exit datpicker

* fix(datepicker): conflicts resolve & leave only esc datepicker close

* feat(datepicker): add test
  • Loading branch information
5earle authored and valorkin committed Dec 13, 2018
1 parent bf07304 commit 3ee6eac
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 28 deletions.
18 changes: 17 additions & 1 deletion src/component-loader/component-loader.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ import {
Type,
ViewContainerRef
} from '@angular/core';

import { PositioningOptions, PositioningService } from 'ngx-bootstrap/positioning';
import { listenToTriggersV2, registerOutsideClick } from 'ngx-bootstrap/utils';

import {
listenToTriggersV2,
registerEscClick,
registerOutsideClick
} from 'ngx-bootstrap/utils';

import { ContentRef } from './content-ref.class';
import { ListenOptions } from './listen-options.model';
import { Subscription } from 'rxjs';
Expand Down Expand Up @@ -250,6 +257,7 @@ export class ComponentLoader<T> {
listen(listenOpts: ListenOptions): ComponentLoader<T> {
this.triggers = listenOpts.triggers || this.triggers;
this._listenOpts.outsideClick = listenOpts.outsideClick;
this._listenOpts.outsideEsc = listenOpts.outsideEsc;
listenOpts.target = listenOpts.target || this._elementRef.nativeElement;

const hide = (this._listenOpts.hide = () =>
Expand Down Expand Up @@ -306,6 +314,14 @@ export class ComponentLoader<T> {
});
});
}
if (this._listenOpts.outsideEsc) {
const target = this._componentRef.location.nativeElement;
this._globalListener = registerEscClick(this._renderer, {
targets: [target, this._elementRef.nativeElement],
outsideEsc: this._listenOpts.outsideEsc,
hide: () => this._listenOpts.hide()
});
}
}

getInnerComponent(): ComponentRef<T> {
Expand Down
1 change: 1 addition & 0 deletions src/component-loader/listen-options.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ListenOptions {
targets?: HTMLElement[];
triggers?: string;
outsideClick?: boolean;
outsideEsc?: boolean;
show?: BsEventCallback;
hide?: BsEventCallback;
toggle?: BsEventCallback;
Expand Down
1 change: 1 addition & 0 deletions src/datepicker/bs-datepicker-input.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,6 @@ export class BsDatepickerInputDirective

hide() {
this._picker.hide();
this._renderer.selectRootElement(this._elRef.nativeElement).blur();
}
}
3 changes: 3 additions & 0 deletions src/datepicker/bs-datepicker.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges {
*/
@Input() container = 'body';

@Input() outsideEsc = true;

/**
* Returns whether or not the datepicker is currently being shown
*/
Expand Down Expand Up @@ -127,6 +129,7 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges {
ngOnInit(): void {
this._datepicker.listen({
outsideClick: this.outsideClick,
outsideEsc: this.outsideEsc,
triggers: this.triggers,
show: () => this.show()
});
Expand Down
73 changes: 47 additions & 26 deletions src/datepicker/bs-datepicker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Component, ViewChild } from '@angular/core';
import { Component, ViewChild, Renderer2 } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { BsDatepickerConfig, BsDatepickerDirective, BsDatepickerModule } from '.';
import { CalendarCellViewModel } from './models';
import { BsDatepickerContainerComponent } from './themes/bs/bs-datepicker-container.component';
import { CalendarCellViewModel } from './models';
import { registerEscClick } from '../utils';

@Component({
selector: 'test-cmp',
Expand All @@ -12,7 +13,7 @@ import { BsDatepickerContainerComponent } from './themes/bs/bs-datepicker-contai
class TestComponent {
@ViewChild(BsDatepickerDirective) datepicker: BsDatepickerDirective;
bsConfig: Partial<BsDatepickerConfig> = {
displayMonths: 2
displayMonths: 2
};
}

Expand Down Expand Up @@ -47,38 +48,58 @@ function getDatepickerContainer(datepicker: BsDatepickerDirective): BsDatepicker
describe('datepicker:', () => {
let fixture: TestFixture;
beforeEach(
async(() => TestBed.configureTestingModule({
declarations: [TestComponent],
imports: [BsDatepickerModule.forRoot()]
}).compileComponents()
));
async(() => TestBed.configureTestingModule({
declarations: [TestComponent],
imports: [BsDatepickerModule.forRoot()]
}).compileComponents()
));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});

it('should display datepicker on show', () => {
const datepicker = showDatepicker(fixture);
expect(getDatepickerContainer(datepicker)).toBeDefined();
const datepicker = showDatepicker(fixture);
expect(getDatepickerContainer(datepicker)).toBeDefined();
});

it('should hide datepicker on hide', () => {
const datepicker = hideDatepicker(fixture);
expect(getDatepickerContainer(datepicker)).toBeNull();
const datepicker = hideDatepicker(fixture);
expect(getDatepickerContainer(datepicker)).toBeNull();
});

it('should select correct year when a month other than selected year is chosen', () => {
const datepicker = showDatepicker(fixture);
const datepickerContainerInstance = getDatepickerContainer(datepicker);
const yearSelection: CalendarCellViewModel = { date: new Date(2017, 1, 1), label: 'label' };
const monthSelection: CalendarCellViewModel = { date: new Date(2018, 1, 1), label: 'label' };
datepickerContainerInstance.yearSelectHandler(yearSelection);
datepickerContainerInstance.monthSelectHandler(monthSelection);
fixture.detectChanges();
datepickerContainerInstance[`_store`]
.select(state => state.view)
.subscribe(view => {
expect(view.date.getFullYear()).toEqual(monthSelection.date.getFullYear());
});
const datepicker = showDatepicker(fixture);
const datepickerContainerInstance = getDatepickerContainer(datepicker);
const yearSelection: CalendarCellViewModel = { date: new Date(2017, 1, 1), label: 'label' };
const monthSelection: CalendarCellViewModel = { date: new Date(2018, 1, 1), label: 'label' };
datepickerContainerInstance.yearSelectHandler(yearSelection);
datepickerContainerInstance.monthSelectHandler(monthSelection);
fixture.detectChanges();
datepickerContainerInstance[`_store`]
.select(state => state.view)
.subscribe(view => {
expect(view.date.getFullYear()).toEqual(monthSelection.date.getFullYear());
});
});

it('should hide on esc', async(() => {
const datepicker = showDatepicker(fixture);
const spy = spyOn(datepicker, 'hide');
const renderer = fixture.componentRef.injector.get<Renderer2>(Renderer2 as any);

registerEscClick(renderer, {
outsideEsc: true,
target: fixture.nativeElement,
hide: () => datepicker.hide()
});

const event = new KeyboardEvent('keyup', {
key: 'Escape'
});

document.dispatchEvent(event);

expect(spy).toHaveBeenCalled();
}));
});
1 change: 1 addition & 0 deletions src/datepicker/bs-daterangepicker-input.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,6 @@ export class BsDaterangepickerInputDirective

hide() {
this._picker.hide();
this._renderer.selectRootElement(this._elRef.nativeElement).blur();
}
}
3 changes: 3 additions & 0 deletions src/datepicker/bs-daterangepicker.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export class BsDaterangepickerDirective
*/
@Input() container = 'body';

@Input() outsideEsc = true;

/**
* Returns whether or not the daterangepicker is currently being shown
*/
Expand Down Expand Up @@ -128,6 +130,7 @@ export class BsDaterangepickerDirective
ngOnInit(): void {
this._datepicker.listen({
outsideClick: this.outsideClick,
outsideEsc: this.outsideEsc,
triggers: this.triggers,
show: () => this.show()
});
Expand Down
8 changes: 7 additions & 1 deletion src/utils/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export * from './triggers';
export { isBs3 } from './theme-provider';
export { LinkedList } from './linked-list.class';
export { listenToTriggersV2, registerOutsideClick } from './triggers';

export {
listenToTriggersV2,
registerOutsideClick,
registerEscClick
} from './triggers';

export { OnChange } from './decorators';
export { setTheme } from './theme-provider';
export { Trigger } from './trigger.class';
Expand Down
22 changes: 22 additions & 0 deletions src/utils/triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ListenOptions {
targets?: HTMLElement[];
triggers?: string;
outsideClick?: boolean;
outsideEsc?: boolean;
show?: BsEventCallback;
hide?: BsEventCallback;
toggle?: BsEventCallback;
Expand Down Expand Up @@ -152,3 +153,24 @@ export function registerOutsideClick(renderer: Renderer2,
options.hide();
});
}

export function registerEscClick(renderer: Renderer2,
options: ListenOptions) {
if (!options.outsideEsc) {
return Function.prototype;
}

return renderer.listen('document', 'keyup.esc', (event: any) => {
if (options.target && options.target.contains(event.target)) {
return undefined;
}
if (
options.targets &&
options.targets.some(target => target.contains(event.target))
) {
return undefined;
}

options.hide();
});
}

0 comments on commit 3ee6eac

Please sign in to comment.