diff --git a/.travis.yml b/.travis.yml index a05effad37..042e3f6a6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,7 @@ script: - npm run build - rm -rf node_modules/ng2-bootstrap - npm i ./dist - #angular-cli test coverage is broken - #- npm run test-coverage + - npm run test-coverage - npm run test after_success: diff --git a/karma.conf.js b/karma.conf.js index 1a74f89667..2d7216a571 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,18 +6,18 @@ const customLaunchers = require('./scripts/sauce-browsers').customLaunchers; module.exports = function (config) { const configuration = { basePath: '', - frameworks: ['jasmine', 'angular-cli'], + frameworks: ['jasmine', '@angular/cli'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-remap-istanbul'), - require('angular-cli/plugins/karma') + require('@angular/cli/plugins/karma') ], files: [ {pattern: './scripts/test.ts', watched: false} ], preprocessors: { - './scripts/test.ts': ['angular-cli'] + './scripts/test.ts': ['@angular/cli'] }, remapIstanbulReporter: { reports: { diff --git a/src/dropdown/dropdown-keyboard-nav.directive.ts b/src/dropdown/dropdown-keyboard-nav.directive.ts deleted file mode 100644 index e63dc05849..0000000000 --- a/src/dropdown/dropdown-keyboard-nav.directive.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Directive, ElementRef, HostListener } from '@angular/core'; - -import { DropdownDirective } from './dropdown.directive'; - -/* tslint:disable-next-line */ -const KeyboardEvent = (global as any).KeyboardEvent as KeyboardEvent; - -@Directive({ - selector: '[dropdown][dropdownKeyboardNav]' -}) -export class KeyboardNavDirective { - protected dd:DropdownDirective; - protected el:ElementRef; - - public constructor(dd:DropdownDirective, el:ElementRef) { - this.dd = dd; - this.el = el; - console.warn('keyboard-nav deprecated'); - dd.keyboardNav = true; - } - - @HostListener('keydown', ['$event']) - public onKeydown(event:KeyboardEvent):void { - if (event.which !== 40 && event.which !== 38) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - let elems = this.dd.menuEl.nativeElement.getElementsByTagName('a'); - - switch (event.which) { - case (40): - if (typeof this.dd.selectedOption !== 'number') { - this.dd.selectedOption = 0; - break; - } - - if (this.dd.selectedOption === elems.length - 1) { - break; - } - - this.dd.selectedOption++; - break; - case (38): - if (typeof this.dd.selectedOption !== 'number') { - return; - } - - if (this.dd.selectedOption === 0) { - // todo: return? - break; - } - - this.dd.selectedOption--; - break; - default: - break; - } - elems[this.dd.selectedOption].nativeElement.focus(); - } -} diff --git a/src/dropdown/dropdown.config.ts b/src/dropdown/dropdown.config.ts index e856ad3b8f..13d8a16cb4 100644 --- a/src/dropdown/dropdown.config.ts +++ b/src/dropdown/dropdown.config.ts @@ -7,5 +7,5 @@ export class DropdownConfig { /** default dropdown auto closing behavior */ public autoClose: string = NONINPUT; /** is keyboard navigation enabled by default */ - public keyboardNav: Boolean = false; + public keyboardNav: boolean = false; } diff --git a/src/dropdown/dropdown.directive.ts b/src/dropdown/dropdown.directive.ts index c033c4e06d..730cc922e7 100644 --- a/src/dropdown/dropdown.directive.ts +++ b/src/dropdown/dropdown.directive.ts @@ -4,7 +4,7 @@ import { } from '@angular/core'; import { isBs3 } from '../utils/ng2-bootstrap-config'; -import { dropdownService } from './dropdown.service'; +import { DropdownService } from './dropdown.service'; import { DropdownConfig } from './dropdown.config'; /** @@ -17,6 +17,7 @@ import { DropdownConfig } from './dropdown.config'; host: {'[class.show]': 'isOpen && !isBs3'} }) export class DropdownDirective implements OnInit, OnDestroy { + private dropdownService: DropdownService; /** if `true` dropdown will be opened */ @HostBinding('class.open') @HostBinding('class.active') @@ -26,6 +27,11 @@ export class DropdownDirective implements OnInit, OnDestroy { } public set isOpen(value: boolean) { + if (this._isOpen === !!value) { + // don't emit events + return; + } + this._isOpen = !!value; // todo: implement after porting position @@ -37,9 +43,9 @@ export class DropdownDirective implements OnInit, OnDestroy { // ready if (this.isOpen) { this.focusToggleElement(); - dropdownService.open(this); + this.dropdownService.open(this); } else { - dropdownService.close(this); + this.dropdownService.close(this); this.selectedOption = void 0; } this.onToggle.emit(this.isOpen); @@ -78,15 +84,21 @@ export class DropdownDirective implements OnInit, OnDestroy { // drop down toggle element public toggleEl: ElementRef; public el: ElementRef; - protected _isOpen: boolean; + protected _isOpen: boolean = false; protected _changeDetector: ChangeDetectorRef; - public constructor(el: ElementRef, ref: ChangeDetectorRef, config: DropdownConfig) { + public constructor( + el: ElementRef, + ref: ChangeDetectorRef, + dropdownService: DropdownService, + config: DropdownConfig + ) { // @Query('dropdownMenu', {descendants: false}) // dropdownMenuList:QueryList) { this.el = el; this._changeDetector = ref; + this.dropdownService = dropdownService; Object.assign(this, config); // todo: bind to route change event } @@ -118,10 +130,14 @@ export class DropdownDirective implements OnInit, OnDestroy { } public show():void { + /** prevent global event handling */ + this.dropdownService.preventEventHandling(); this.isOpen = true; } public hide():void { + /** prevent global event handling */ + this.dropdownService.preventEventHandling(); this.isOpen = false; } diff --git a/src/dropdown/dropdown.module.ts b/src/dropdown/dropdown.module.ts index 40e994c503..0e14985dd0 100644 --- a/src/dropdown/dropdown.module.ts +++ b/src/dropdown/dropdown.module.ts @@ -4,6 +4,7 @@ import { DropdownMenuDirective } from './dropdown-menu.directive'; import { DropdownToggleDirective } from './dropdown-toggle.directive'; import { DropdownDirective } from './dropdown.directive'; import { DropdownConfig } from './dropdown.config'; +import { DropdownService } from './dropdown.service'; @NgModule({ declarations: [DropdownDirective, DropdownMenuDirective, DropdownToggleDirective], @@ -11,6 +12,6 @@ import { DropdownConfig } from './dropdown.config'; }) export class DropdownModule { public static forRoot(): ModuleWithProviders { - return {ngModule: DropdownModule, providers: [DropdownConfig]}; + return {ngModule: DropdownModule, providers: [DropdownConfig, DropdownService]}; } } diff --git a/src/dropdown/dropdown.service.ts b/src/dropdown/dropdown.service.ts index cc3d616727..777dbb7552 100644 --- a/src/dropdown/dropdown.service.ts +++ b/src/dropdown/dropdown.service.ts @@ -1,20 +1,25 @@ +import { Injectable } from '@angular/core'; + +import { DropdownDirective } from './dropdown.directive'; + export const ALWAYS = 'always'; export const DISABLED = 'disabled'; export const OUTSIDECLICK = 'outsideClick'; export const NONINPUT = 'nonInput'; -import { DropdownDirective } from './dropdown.directive'; - /* tslint:disable-next-line */ const KeyboardEvent = (global as any).KeyboardEvent as KeyboardEvent; /* tslint:disable-next-line */ const MouseEvent = (global as any).MouseEvent as MouseEvent; +@Injectable() export class DropdownService { - protected openScope:DropdownDirective; + private openScope:DropdownDirective; + + private closeDropdownBind:EventListener = this.closeDropdown.bind(this); + private keybindFilterBind:EventListener = this.keybindFilter.bind(this); - protected closeDropdownBind:EventListener = this.closeDropdown.bind(this); - protected keybindFilterBind:EventListener = this.keybindFilter.bind(this); + private suspendedEvent: any; public open(dropdownScope:DropdownDirective):void { if (!this.openScope) { @@ -39,34 +44,44 @@ export class DropdownService { window.document.removeEventListener('keydown', this.keybindFilterBind); } - protected closeDropdown(event:MouseEvent):void { - if (!this.openScope) { - return; - } - - if (event && this.openScope.autoClose === DISABLED) { - return; - } - - if (event && this.openScope.toggleEl && - this.openScope.toggleEl.nativeElement.contains(event.target)) { - return; - } - - if (event && this.openScope.autoClose === NONINPUT && - this.openScope.menuEl && - /input|textarea/i.test((event.target as any).tagName) && - this.openScope.menuEl.nativeElement.contains(event.target)) { - return; - } - - if (event && this.openScope.autoClose === OUTSIDECLICK && - this.openScope.menuEl && - this.openScope.menuEl.nativeElement.contains(event.target)) { - return; - } + public preventEventHandling(): void { + clearTimeout(this.suspendedEvent); + } - this.openScope.isOpen = false; + protected closeDropdown(event:MouseEvent):void { + this.suspendedEvent = setTimeout(() => { + if (!this.openScope) { + return; + } + + if (event && this.openScope.autoClose === DISABLED) { + return; + } + + if (event && this.openScope.toggleEl && + this.openScope.toggleEl.nativeElement.contains(event.target)) { + return; + } + + if (event && this.openScope.autoClose === NONINPUT && + this.openScope.menuEl && + /input|textarea/i.test((event.target as any).tagName) && + this.openScope.menuEl.nativeElement.contains(event.target)) { + return; + } + + if (event && this.openScope.autoClose === OUTSIDECLICK && + this.openScope.menuEl && + this.openScope.menuEl.nativeElement.contains(event.target)) { + return; + } + + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + this.openScope.isOpen = false; + }, 0); } protected keybindFilter(event:KeyboardEvent):void { @@ -84,5 +99,3 @@ export class DropdownService { } } } - -export let dropdownService = new DropdownService(); diff --git a/src/spec/dropdown.directive.spec.ts b/src/spec/dropdown.directive.spec.ts index 1e572ab77f..eed1ad998b 100644 --- a/src/spec/dropdown.directive.spec.ts +++ b/src/spec/dropdown.directive.spec.ts @@ -1,6 +1,6 @@ /* tslint:disable:max-file-line-count */ import { Component } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { DropdownModule } from '../dropdown/dropdown.module'; import { DropdownConfig } from '../dropdown/dropdown.config'; @@ -77,7 +77,7 @@ describe('Directive: Dropdown', () => { expect(element.querySelector('.dropdown').classList).not.toContain('open'); }); - it('should close by click on nonInput menu item', () => { + it('should close by click on nonInput menu item', fakeAsync(() => { const html = `
@@ -100,10 +100,10 @@ describe('Directive: Dropdown', () => { fixture.detectChanges(); expect(element.querySelector('.dropdown').classList).toContain('open'); element.querySelector('li').click(); + tick(); fixture.detectChanges(); expect(element.querySelector('.dropdown').classList).not.toContain('open'); - - }); + })); it('should not close by click on input or textarea menu item', () => { const html = ` diff --git a/src/spec/ng-bootstrap/dropdown.spec.ts b/src/spec/ng-bootstrap/dropdown.spec.ts index 18f3a5b218..59d0a360bd 100644 --- a/src/spec/ng-bootstrap/dropdown.spec.ts +++ b/src/spec/ng-bootstrap/dropdown.spec.ts @@ -1,15 +1,11 @@ /* tslint:disable:max-classes-per-file max-file-line-count component-class-suffix */ // revision 6c0b585aa4a7c13c44631915d13488e6967162f4 -import { TestBed, ComponentFixture, inject } from '@angular/core/testing'; +import { TestBed, ComponentFixture, inject, fakeAsync, tick } from '@angular/core/testing'; import { createGenericTestComponent } from './test/common'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; -// import { DropdownModule } from './dropdown.module'; -// import { DropdownDirective } from './dropdown'; -// import { DropdownConfig } from './dropdown-config'; - import { DropdownConfig, DropdownModule, DropdownDirective } from '../../dropdown'; const createTestComponent = (html: string) => @@ -19,17 +15,18 @@ function getDropdownEl(tc: any): any { return tc.querySelector(`[dropdown]`); } -xdescribe('bs-dropdown', () => { +describe('bs-dropdown', () => { beforeEach(() => { - TestBed.configureTestingModule({declarations: [TestComponent], imports: [DropdownModule.forRoot()]}); + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [DropdownModule.forRoot()], + }); }); - xit('should initialize inputs with provided config', () => { - const defaultConfig = new DropdownConfig(); - const dropdown = new DropdownDirective(defaultConfig); - expect(dropdown.up).toBe(defaultConfig.up); - expect(dropdown.autoClose).toBe(defaultConfig.autoClose); - }); + it('should initialize inputs with provided config', inject([DropdownConfig], (defaultConfig: DropdownConfig) => { + const dropdown = new DropdownDirective(undefined, undefined, undefined, defaultConfig); + expect(dropdown.autoClose).toBe(defaultConfig.autoClose, 'unexpected autoclose setting'); + })); it('should be closed and down by default', () => { const html = `
`; @@ -41,17 +38,8 @@ xdescribe('bs-dropdown', () => { expect(getDropdownEl(compiled)).not.toHaveCssClass('open'); }); - xit('should be up if up input is true', () => { - const html = `
`; - - const fixture = createTestComponent(html); - const compiled = fixture.nativeElement; - - expect(getDropdownEl(compiled)).toHaveCssClass('dropup'); - }); - - xit('should be open initially if open expression is true', () => { - const html = `
`; + it('should be open initially if open expression is true', () => { + const html = `
`; const fixture = createTestComponent(html); const compiled = fixture.nativeElement; @@ -132,7 +120,7 @@ xdescribe('bs-dropdown', () => { expect(fixture.componentInstance.isOpen).toBe(false); }); - it('should not raise open events if open state does not change', () => { + it('should not raise open events if open state does not change', fakeAsync(() => { const html = ` @@ -157,6 +145,7 @@ xdescribe('bs-dropdown', () => { buttonEls[0].click(); // open an opened one fixture.detectChanges(); + tick(); expect(fixture.componentInstance.isOpen).toBe(true); expect(fixture.componentInstance.stateChanges).toEqual([true]); @@ -164,10 +153,10 @@ xdescribe('bs-dropdown', () => { fixture.detectChanges(); expect(fixture.componentInstance.isOpen).toBe(false); expect(fixture.componentInstance.stateChanges).toEqual([true, false]); - }); + })); }); -xdescribe('bs-dropdown-toggle', () => { +describe('bs-dropdown-toggle', () => { beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [DropdownModule.forRoot()]}); }); @@ -222,7 +211,7 @@ xdescribe('bs-dropdown-toggle', () => { expect(dropdownEl).not.toHaveCssClass('open'); }); - it('should close on outside click', () => { + it('should close on outside click', fakeAsync(() => { const html = `
`; const fixture = createTestComponent(html); @@ -234,11 +223,12 @@ xdescribe('bs-dropdown-toggle', () => { expect(dropdownEl).toHaveCssClass('open'); buttonEl.click(); + tick(); fixture.detectChanges(); expect(dropdownEl).not.toHaveCssClass('open'); - }); + })); - it('should not close on outside click if autoClose is set to false', () => { + it('should not close on outside click if autoClose is set to false', fakeAsync(() => { const html = `
`; const fixture = createTestComponent(html); @@ -251,10 +241,11 @@ xdescribe('bs-dropdown-toggle', () => { buttonEl.click(); fixture.detectChanges(); + tick(); expect(dropdownEl).toHaveCssClass('open'); - }); + })); - it('should close on ESC', () => { + it('should close on ESC', fakeAsync(() => { const html = `
@@ -269,12 +260,16 @@ xdescribe('bs-dropdown-toggle', () => { fixture.detectChanges(); expect(dropdownEl).toHaveCssClass('open'); - fixture.debugElement.query(By.directive(DropdownDirective)).triggerEventHandler('keyup.esc', {}); + let event = document.createEvent('CustomEvent') as Event; + event.initEvent('keydown', true, true); + event.which = 27; + document.dispatchEvent(event); + tick(); fixture.detectChanges(); expect(dropdownEl).not.toHaveCssClass('open'); - }); + })); - it('should not close on ESC if autoClose is set to false', () => { + it('should not close on ESC if autoClose is set to false', fakeAsync(() => { const html = `
@@ -292,9 +287,9 @@ xdescribe('bs-dropdown-toggle', () => { fixture.debugElement.query(By.directive(DropdownDirective)).triggerEventHandler('keyup.esc', {}); fixture.detectChanges(); expect(dropdownEl).toHaveCssClass('open'); - }); + })); - it('should close on item click if autoClose is set to false', () => { + it('should close on item click if autoClose is set to false', fakeAsync(() => { const html = `
@@ -312,11 +307,12 @@ xdescribe('bs-dropdown-toggle', () => { expect(dropdownEl).toHaveCssClass('open'); linkEl.click(); + tick(); fixture.detectChanges(); - expect(dropdownEl).toHaveCssClass('open'); - }); + expect(dropdownEl).not.toHaveCssClass('open'); + })); - it('should close on item click', () => { + it('should close on item click', fakeAsync(() => { const html = `
@@ -334,9 +330,10 @@ xdescribe('bs-dropdown-toggle', () => { expect(dropdownEl).toHaveCssClass('open'); linkEl.click(); + tick(); fixture.detectChanges(); expect(dropdownEl).not.toHaveCssClass('open'); - }); + })); it('should close on other dropdown click', () => { const html = ` @@ -373,48 +370,6 @@ xdescribe('bs-dropdown-toggle', () => { expect(dropdownEls[0]).not.toHaveCssClass('open'); expect(dropdownEls[1]).toHaveCssClass('open'); }); - - describe('Custom config', () => { - let config: DropdownConfig; - - beforeEach(() => { - TestBed.configureTestingModule({imports: [DropdownModule.forRoot()]}); - TestBed.overrideComponent(TestComponent, {set: {template: '
'}}); - }); - - beforeEach(inject([DropdownConfig], (c: DropdownConfig) => { - config = c; - config.up = true; - })); - - it('should initialize inputs with provided config', () => { - const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); - - const compiled = fixture.nativeElement; - - expect(getDropdownEl(compiled)).toHaveCssClass('dropup'); - }); - }); - - describe('Custom config as provider', () => { - let config = new DropdownConfig(); - config.up = true; - - beforeEach(() => { - TestBed.configureTestingModule( - {imports: [DropdownModule.forRoot()], providers: [{provide: DropdownConfig, useValue: config}]}); - }); - - it('should initialize inputs with provided config as provider', () => { - const fixture = createTestComponent('
'); - fixture.detectChanges(); - - const compiled = fixture.nativeElement; - - expect(getDropdownEl(compiled)).toHaveCssClass('dropup'); - }); - }); }); @Component({selector: 'test-cmp', template: ''})