From 5aff41bbfc00a116a92928da6a5949fbce81ce5f Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Mon, 18 Jun 2018 11:09:05 +0300 Subject: [PATCH] refactor: use single input to control open/close --- README.md | 2 +- src/ng-select/ng-select.component.html | 8 ++-- src/ng-select/ng-select.component.spec.ts | 58 +++++++++++------------ src/ng-select/ng-select.component.ts | 41 ++++++++-------- 4 files changed, 56 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 743d68f9d..7f40aa177 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ map: { | loadingText | `string` | `Loading...` | no | Set custom text when for loading items | | labelForId | `string` | `-` | no | Id to associate control with label. | | [markFirst] | `boolean` | `true` | no | Marks first item as focused when opening/filtering. | -| [isOpen] | `boolean` | `-` | no | Allows to control whether dropdown should open or close. `True` - won't close. `False` - won't open. | +| [isOpen] | `boolean` | `-` | no | Allows manual control of dropdown opening and closing. `True` - won't close. `False` - won't open. | | maxSelectedItems | `number` | none | no | When multiple = true, allows to set a limit number of selection. | | [hideSelected] | `boolean` | `false` | no | Allows to hide selected items. | | [multiple] | `boolean` | `false` | no | Allows to select multiple items. | diff --git a/src/ng-select/ng-select.component.html b/src/ng-select/ng-select.component.html index fb1b963eb..dfe1b9cf0 100644 --- a/src/ng-select/ng-select.component.html +++ b/src/ng-select/ng-select.component.html @@ -34,9 +34,9 @@ (blur)="onInputBlur()" (change)="$event.stopPropagation()" role="combobox" - [attr.aria-expanded]="opened" - [attr.aria-owns]="opened ? dropdownId : null" - [attr.aria-activedescendant]="opened ? itemsList?.markedItem?.htmlId : null"> + [attr.aria-expanded]="isOpen" + [attr.aria-owns]="isOpen ? dropdownId : null" + [attr.aria-activedescendant]="isOpen ? itemsList?.markedItem?.htmlId : null"> @@ -51,7 +51,7 @@ - { - expect(fixture.componentInstance.select.opened).toBeFalsy(); + expect(fixture.componentInstance.select.isOpen).toBeFalsy(); }) })); @@ -941,7 +941,7 @@ describe('NgSelectComponent', function () { fixture.detectChanges(); fixture.whenStable().then(() => { - expect(fixture.componentInstance.select.opened).toBeTruthy(); + expect(fixture.componentInstance.select.isOpen).toBeTruthy(); }) })); @@ -957,7 +957,7 @@ describe('NgSelectComponent', function () { selectOption(fixture, KeyCode.ArrowDown, 0); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBeTruthy(); + expect(fixture.componentInstance.select.isOpen).toBeTruthy(); })); }); @@ -981,13 +981,13 @@ describe('NgSelectComponent', function () { describe('space', () => { it('should open dropdown', () => { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); - expect(select.opened).toBe(true); + expect(select.isOpen).toBe(true); }); it('should not open dropdown when isOpen is false', () => { - select.isOpen = false; + select.ngOnChanges({ isOpen: { currentValue: false }}); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); - expect(select.opened).toBeFalsy(); + expect(select.isOpen).toBeFalsy(); }); it('should open empty dropdown if no items', fakeAsync(() => { @@ -1095,9 +1095,9 @@ describe('NgSelectComponent', function () { describe('esc', () => { it('should close opened dropdown', () => { - select.opened = true; + select.isOpen = true; triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Esc); - expect(select.opened).toBe(false); + expect(select.isOpen).toBe(false); }); }); @@ -1107,7 +1107,7 @@ describe('NgSelectComponent', function () { tick(200); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab); - expect(select.opened).toBeFalsy() + expect(select.isOpen).toBeFalsy() })); it('should close dropdown when [selectOnTab]="false"', fakeAsync(() => { @@ -1116,7 +1116,7 @@ describe('NgSelectComponent', function () { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab); expect(select.selectedItems).toEqual([]); - expect(select.opened).toBeFalsy(); + expect(select.isOpen).toBeFalsy(); })); it('should close dropdown and keep selected value', fakeAsync(() => { @@ -1129,7 +1129,7 @@ describe('NgSelectComponent', function () { value: fixture.componentInstance.cities[0] })]; expect(select.selectedItems).toEqual(result); - expect(select.opened).toBeFalsy() + expect(select.isOpen).toBeFalsy() })); it('should mark first item on filter when tab', fakeAsync(() => { @@ -1230,14 +1230,14 @@ describe('NgSelectComponent', function () { describe('enter', () => { it('should open dropdown when it is closed', () => { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter); - expect(select.opened).toBe(true); + expect(select.isOpen).toBe(true); }); it('should select option and close dropdown', () => { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter); expect(select.selectedItems[0].value).toEqual(fixture.componentInstance.cities[0]); - expect(select.opened).toBe(false); + expect(select.isOpen).toBe(false); }); }); }); @@ -1261,22 +1261,22 @@ describe('NgSelectComponent', function () { it('should close dropdown if opened and clicked outside dropdown container', fakeAsync(() => { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); - expect(fixture.componentInstance.select.opened).toBeTruthy(); + expect(fixture.componentInstance.select.isOpen).toBeTruthy(); document.getElementById('outside').click(); let event = new MouseEvent('mousedown', { bubbles: true }); document.getElementById('outside').dispatchEvent(event); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBeFalsy(); + expect(fixture.componentInstance.select.isOpen).toBeFalsy(); })); it('should prevent dropdown close if clicked on select', fakeAsync(() => { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); - expect(select.opened).toBeTruthy(); + expect(select.isOpen).toBeTruthy(); document.getElementById('select').click(); let event = new MouseEvent('mousedown', { bubbles: true }); document.getElementById('select').dispatchEvent(event); tickAndDetectChanges(fixture); - expect(select.opened).toBeTruthy(); + expect(select.isOpen).toBeTruthy(); })); }); @@ -1618,7 +1618,7 @@ describe('NgSelectComponent', function () { tickAndDetectChanges(fixture); clickArrow(); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBe(false); + expect(fixture.componentInstance.select.isOpen).toBe(false); expect((fixture.componentInstance.select.selectedItems).length).toBe(2); })); }); @@ -1636,7 +1636,7 @@ describe('NgSelectComponent', function () { selectOption(fixture, KeyCode.ArrowDown, 1); expect(select.selectedItems.length).toBe(3); expect(select.itemsList.filteredItems.length).toBe(0); - expect(select.opened).toBeFalsy(); + expect(select.isOpen).toBeFalsy(); })); it('should not open dropdown when all items are selected', fakeAsync(() => { @@ -1645,7 +1645,7 @@ describe('NgSelectComponent', function () { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); expect(select.selectedItems.length).toBe(3); expect(select.itemsList.filteredItems.length).toBe(0); - expect(select.opened).toBeFalsy(); + expect(select.isOpen).toBeFalsy(); })); it('should open dropdown when all items are selected and tagging is enabled', fakeAsync(() => { @@ -1653,7 +1653,7 @@ describe('NgSelectComponent', function () { fixture.componentInstance.cities = []; tickAndDetectChanges(fixture); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); - expect(select.opened).toBeTruthy(); + expect(select.isOpen).toBeTruthy(); })); it('should not insert option back to list if it is newly created option', fakeAsync(() => { @@ -1941,12 +1941,12 @@ describe('NgSelectComponent', function () { // open selectInput.triggerEventHandler('mousedown', createEvent({ target: { className: '' } })); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBe(true); + expect(fixture.componentInstance.select.isOpen).toBe(true); // close selectInput.triggerEventHandler('mousedown', createEvent({ target: { className: '' } })); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBe(false); + expect(fixture.componentInstance.select.isOpen).toBe(false); })); it('should not filter when searchable false', fakeAsync(() => { @@ -2197,7 +2197,7 @@ describe('NgSelectComponent', function () { tickAndDetectChanges(fixture); fixture.componentInstance.filter.subscribe(); fixture.componentInstance.select.open(); - expect(fixture.componentInstance.select.opened).toBeTruthy(); + expect(fixture.componentInstance.select.isOpen).toBeTruthy(); })); }); @@ -2538,7 +2538,7 @@ describe('NgSelectComponent', function () { it('should not open dropdown', fakeAsync(() => { triggerMousedown(); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBe(false); + expect(fixture.componentInstance.select.isOpen).toBe(false); })); it('clear button should not appear if select is disabled', fakeAsync(() => { @@ -2573,7 +2573,7 @@ describe('NgSelectComponent', function () { it('should not open dropdown', fakeAsync(() => { triggerMousedown(); tickAndDetectChanges(fixture); - expect(select.opened).toBe(false); + expect(select.isOpen).toBe(false); })); it('should focus dropdown', fakeAsync(() => { @@ -2605,17 +2605,17 @@ describe('NgSelectComponent', function () { // open triggerMousedown(); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBe(true); + expect(fixture.componentInstance.select.isOpen).toBe(true); // close triggerMousedown(); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBe(false); + expect(fixture.componentInstance.select.isOpen).toBe(false); // open triggerMousedown(); tickAndDetectChanges(fixture); - expect(fixture.componentInstance.select.opened).toBe(true); + expect(fixture.componentInstance.select.isOpen).toBe(true); })); }); }); diff --git a/src/ng-select/ng-select.component.ts b/src/ng-select/ng-select.component.ts index afc294ef8..90ef9866a 100644 --- a/src/ng-select/ng-select.component.ts +++ b/src/ng-select/ng-select.component.ts @@ -87,7 +87,6 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C @Input() appendTo: string; @Input() loading = false; @Input() closeOnSelect = true; - @Input() isOpen: boolean; @Input() hideSelected = false; @Input() selectOnTab = false; @Input() maxSelectedItems: number; @@ -102,6 +101,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C @Input() @HostBinding('class.ng-select-multiple') multiple = false; @Input() @HostBinding('class.ng-select-taggable') addTag: boolean | AddTagFn = false; @Input() @HostBinding('class.ng-select-searchable') searchable = true; + @Input() @HostBinding('class.ng-select-opened') isOpen = false; @Input() get compareWith() { return this._compareWith; } @@ -139,7 +139,6 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C @ContentChildren(NgOptionComponent, { descendants: true }) ngOptions: QueryList; @ViewChild('filterInput') filterInput: ElementRef; - @HostBinding('class.ng-select-opened') opened = false; @HostBinding('class.ng-select-disabled') disabled = false; @HostBinding('class.ng-select-filtered') get filtered() { return !!this.filterValue && this.searchable }; @@ -152,6 +151,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C private _defaultLabel = 'label'; private _primitive: boolean; private _focused: boolean; + private _manualOpen: boolean; private _pressedKeys: string[] = []; private _compareWith: CompareWithFn; @@ -198,6 +198,9 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C if (changes.items) { this._setItems(changes.items.currentValue || []); } + if (changes.isOpen) { + this._manualOpen = true; + } } ngAfterViewInit() { @@ -274,7 +277,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } handleArrowClick() { - if (this.opened) { + if (this.isOpen) { this.close(); } else { this.open(); @@ -321,7 +324,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } toggle() { - if (!this.opened) { + if (!this.isOpen) { this.open(); } else { this.close(); @@ -329,14 +332,14 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } open() { - if (this.disabled || this.opened || this.itemsList.maxItemsSelected || this.isOpen === false) { + if (this.disabled || this.isOpen || this.itemsList.maxItemsSelected || this._manualOpen) { return; } if (!this._isTypeahead && !this.addTag && this.itemsList.noItemsToSelect) { return; } - this.opened = true; + this.isOpen = true; this.itemsList.markSelectedOrDefault(this.markFirst); this.openEvent.emit(); if (!this.filterValue) { @@ -346,10 +349,10 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } close() { - if (!this.opened || this.isOpen) { + if (!this.isOpen || this._manualOpen) { return; } - this.opened = false; + this.isOpen = false; this._clearSearch(); this._onTouched(); this.closeEvent.emit(); @@ -440,7 +443,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C this.typeahead.next(this.filterValue); } else { this.itemsList.filter(this.filterValue); - if (this.isOpen !== false) { + if (this.isOpen) { this.itemsList.markSelectedOrDefault(this.markFirst); } } @@ -455,7 +458,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C onInputBlur() { (this.elementRef.nativeElement).classList.remove('ng-select-focused'); this.blurEvent.emit(null); - if (!this.opened && !this.disabled) { + if (!this.isOpen && !this.disabled) { this._onTouched(); } this._focused = false; @@ -488,10 +491,10 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C if (items.length > 0 && this.hasValue) { this.itemsList.mapSelectedItems(); } - if (this.opened && isDefined(this.filterValue) && !this._isTypeahead) { + if (this.isOpen && isDefined(this.filterValue) && !this._isTypeahead) { this.itemsList.filter(this.filterValue); } - if (this._isTypeahead || this.opened) { + if (this._isTypeahead || this.isOpen) { this.itemsList.markSelectedOrDefault(this.markFirst); } } @@ -603,7 +606,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C .subscribe(term => { const item = this.itemsList.findByLabel(term); if (item) { - if (this.opened) { + if (this.isOpen) { this.itemsList.markItem(item); this._cd.markForCheck(); } else { @@ -651,21 +654,21 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } private _scrollToMarked() { - if (!this.opened || !this.dropdownPanel) { + if (!this.isOpen || !this.dropdownPanel) { return; } this.dropdownPanel.scrollInto(this.itemsList.markedItem); } private _scrollToTag() { - if (!this.opened || !this.dropdownPanel) { + if (!this.isOpen || !this.dropdownPanel) { return; } this.dropdownPanel.scrollIntoTag(); } private _handleTab($event: KeyboardEvent) { - if (!this.opened) { + if (!this.isOpen) { return; } if (this.selectOnTab) { @@ -684,7 +687,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } private _handleEnter($event: KeyboardEvent) { - if (this.opened || this.isOpen === false) { + if (this.isOpen || this._manualOpen) { if (this.itemsList.markedItem) { this.toggleItem(this.itemsList.markedItem); } else if (this.addTag && this.filterValue) { @@ -698,7 +701,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } private _handleSpace($event: KeyboardEvent) { - if (this.opened) { + if (this.isOpen) { return; } this.open(); @@ -718,7 +721,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } private _handleArrowUp($event: KeyboardEvent) { - if (!this.opened) { + if (!this.isOpen) { return; }