diff --git a/CHANGELOG.md b/CHANGELOG.md index 088e9491748..04ea0147c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Ignite UI for Angular Change Log All notable changes for each version of this project will be documented in this file. +## 7.2.0 +- `igxCombo` + - **Breaking Change** `combo.value` is now only a getter. ## 7.1.2 ### Features - `igx-circular-bar` and `igx-linear-bar` now feature an indeterminate input property. When this property is set to true the indicator will be continually growing and shrinking along the track. diff --git a/projects/igniteui-angular/src/lib/combo/README.md b/projects/igniteui-angular/src/lib/combo/README.md index 53907670c09..e59bff60758 100644 --- a/projects/igniteui-angular/src/lib/combo/README.md +++ b/projects/igniteui-angular/src/lib/combo/README.md @@ -55,7 +55,7 @@ The service, should inform the combo for the total items that are on the server ### Value Binding -If we want to use a two-way data-binding, we could just use `ngModule` like this: +If we want to use a two-way data-binding, we could just use `ngModel` like this: ```html @@ -210,7 +210,6 @@ When igxCombo is opened, allow custom values are enabled and add item button is |--------------------------|---------------------------------------------------|-----------------------------| | `id` | combo id | string | | `data` | combo data source | any | -| `value` | combo value | string | | `allowCustomValue` | enables/disables combo custom value | boolean | | `filterable` | enables/disables combo drop down filtering - enabled by default | boolean | | `valueKey` | combo value data source property | string | @@ -231,6 +230,11 @@ When igxCombo is opened, allow custom values are enabled and add item button is | `type` | Combo style. - "line", "box", "border", "search" | string | | `valid` | gets if control is valid, when used in a form | boolean | +### Getters +| Name | Description | Type | +|--------------------------|---------------------------------------------------|-----------------------------| +| `value` | the value of the combo text field | string | + ### Outputs | Name | Description | Cancelable | Parameters | @@ -256,3 +260,4 @@ When igxCombo is opened, allow custom values are enabled and add item button is | `deselectItems` | Deselect defined items | `void` | items: `Array` | | `selectAllItems` | Select all (filtered) items | `void` | ignoreFilter?: `boolean` - if `true` selects **all** values | | `deselectAllItems` | Deselect (filtered) all items | `void` | ignoreFilter?: `boolean` - if `true` deselects **all** values | +| `setSelectedItem` | Toggles (select/deselect) an item by key | `void` | itemID: any, select = true, event?: Event | diff --git a/projects/igniteui-angular/src/lib/combo/combo-add-item.component.ts b/projects/igniteui-angular/src/lib/combo/combo-add-item.component.ts new file mode 100644 index 00000000000..d85b61bce22 --- /dev/null +++ b/projects/igniteui-angular/src/lib/combo/combo-add-item.component.ts @@ -0,0 +1,20 @@ +import { IgxComboItemComponent } from './combo-item.component'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'igx-combo-add-item', + template: '', + providers: [{ provide: IgxComboItemComponent, useExisting: IgxComboAddItemComponent}] +}) +export class IgxComboAddItemComponent extends IgxComboItemComponent { + get isSelected(): boolean { + return false; + } + set isSelected(value: boolean) { + } + + clicked(event?) { + this.comboAPI.disableTransitions = false; + this.comboAPI.add_custom_item(); + } +} diff --git a/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts b/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts index d600d1c3c5e..6795132f048 100644 --- a/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts +++ b/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts @@ -1,32 +1,33 @@ import { - ChangeDetectorRef, Component, ContentChild, - ElementRef, forwardRef, Inject, QueryList, EventEmitter, OnDestroy, AfterViewInit + ChangeDetectorRef, Component, ContentChild, ElementRef, forwardRef, Inject, QueryList, OnDestroy, AfterViewInit, Input, ContentChildren } from '@angular/core'; import { takeUntil, take } from 'rxjs/operators'; -import { IgxComboItemComponent } from './combo-item.component'; -import { IgxSelectionAPIService } from '../core/selection'; import { IgxForOfDirective } from '../directives/for-of/for_of.directive'; -import { Subject } from 'rxjs'; import { CancelableEventArgs } from '../core/utils'; import { IgxComboBase, IGX_COMBO_COMPONENT } from './combo.common'; -import { IgxDropDownBase, IgxDropDownItemBase } from '../drop-down/drop-down.base'; import { Navigate } from '../drop-down/drop-down.common'; +import { IDropDownBase, IGX_DROPDOWN_BASE } from '../drop-down/drop-down.common'; +import { IgxDropDownComponent } from '../drop-down/drop-down.component'; +import { DropDownActionKey } from '../drop-down/drop-down-navigation.directive'; +import { IgxComboAddItemComponent } from './combo-add-item.component'; +import { IgxComboAPIService } from './combo.api'; +import { IgxDropDownItemBase } from '../drop-down/drop-down-item.base'; +import { IgxSelectionAPIService } from '../core/selection'; +import { IgxComboItemComponent } from './combo-item.component'; /** @hidden */ @Component({ selector: 'igx-combo-drop-down', templateUrl: '../drop-down/drop-down.component.html', - providers: [{ provide: IgxDropDownBase, useExisting: IgxComboDropDownComponent }] + providers: [{ provide: IGX_DROPDOWN_BASE, useExisting: IgxComboDropDownComponent }] }) -export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterViewInit, OnDestroy { - private _children: QueryList; - private _scrollPosition = 0; - private destroy$ = new Subject(); +export class IgxComboDropDownComponent extends IgxDropDownComponent implements IDropDownBase, OnDestroy, AfterViewInit { constructor( protected elementRef: ElementRef, protected cdr: ChangeDetectorRef, protected selection: IgxSelectionAPIService, - @Inject(IGX_COMBO_COMPONENT) public combo: IgxComboBase) { + @Inject(IGX_COMBO_COMPONENT) public combo: IgxComboBase, + protected comboAPI: IgxComboAPIService) { super(elementRef, cdr, selection); } @@ -52,27 +53,10 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV this.items.length - 1; } - /** - * Event emitter overrides - * - * @hidden - */ - public onOpened = this.combo.onOpened; - - /** - * @hidden - */ - public onOpening = this.combo.onOpening; - - /** - * @hidden - */ - public onClosing = this.combo.onClosing; + @ContentChildren(IgxComboItemComponent, { descendants: true }) + protected children: QueryList = null; - /** - * @hidden - */ - public onClosed = this.combo.onClosed; + private _scrollPosition = 0; /** * @hidden @@ -83,19 +67,8 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV /** * @hidden */ - protected get children(): QueryList { - return this.combo.children; - } - - protected set children(list: QueryList) { - this._children = list; - } - - /** - * @hidden - */ - onFocus() { - this._focusedItem = this._focusedItem ? this._focusedItem : this.items.length ? this.items[0] : this.children.first; + public onFocus() { + this._focusedItem = this._focusedItem || this.items[0]; if (this._focusedItem) { this._focusedItem.isFocused = true; } @@ -104,7 +77,7 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV /** * @hidden */ - onBlur(evt?) { + public onBlur(evt?) { if (this._focusedItem) { this._focusedItem.isFocused = false; this._focusedItem = null; @@ -114,15 +87,13 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV /** * @hidden */ - public get selectedItem(): any[] { - const sel = this.selection.get(this.combo.id); - return sel ? Array.from(sel) : []; + public onToggleOpened() { + this.onOpened.emit(); } - /** * @hidden */ - navigatePrev() { + public navigatePrev() { if (this._focusedItem.index === 0 && this.verticalScrollContainer.state.startIndex === 0) { this.combo.focusSearchInput(false); } else { @@ -133,7 +104,7 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV /** * @hidden */ - navigateFirst() { + public navigateFirst() { const vContainer = this.verticalScrollContainer; if (vContainer.state.startIndex === 0) { super.navigateItem(0); @@ -150,7 +121,7 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV /** * @hidden */ - navigateLast() { + public navigateLast() { const vContainer = this.verticalScrollContainer; const scrollTarget = this.combo.totalItemCount ? this.combo.totalItemCount - 1 : @@ -184,41 +155,12 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV /** * @hidden */ - setSelectedItem(itemID: any, select = true) { - this.combo.setSelectedItem(itemID, select); - } - - /** - * @hidden - */ - selectItem(item: IgxComboItemComponent, event?: Event) { - if (item.value === 'ADD ITEM') { - if (event) { - this.combo.addItemToCollection(); - } - } else { - this.setSelectedItem(item.itemID); - this._focusedItem = item; - } - } - - /** - * @hidden - */ - public navigateItem(newIndex: number, direction?: number) { - const vContainer = this.verticalScrollContainer; - const notVirtual = vContainer.dc.instance.notVirtual; - if (notVirtual || !direction) { // If list has no scroll OR no direction is passed - super.navigateItem(newIndex); // use default scroll - } else if (vContainer && vContainer.totalItemCount && vContainer.totalItemCount !== 0) { - this.navigateRemoteItem(direction); - } else { - if (direction === Navigate.Up) { // Navigate UP - this.navigateUp(newIndex); - } else if (direction === Navigate.Down) { // Navigate DOWN - this.navigateDown(newIndex); - } + public selectItem(item: IgxDropDownItemBase) { + if (item === null || item === undefined) { + return; } + this.comboAPI.set_selected_item(item.itemID); + this._focusedItem = item; } private navigateDown(newIndex?: number) { @@ -229,7 +171,7 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV const items = this.items; const children = this.children.toArray(); if (focusedItem) { - if (focusedItem.value === 'ADD ITEM') { return; } + if (this.isAddItemFocused()) { return; } if (focusedItem.value === allData[allData.length - 1]) { this.focusAddItemButton(); return; @@ -268,14 +210,14 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV const vContainer = this.verticalScrollContainer; const allData = vContainer.igxForOf; const focusedItem = this.focusedItem; - if (focusedItem.value === allData.find(e => !e.isHeader && !e.hidden).value) { // If this is the very first non-header item + if (focusedItem.value === allData.find(e => !e.isHeader && !e.hidden)) { // If this is the very first non-header item this.focusComboSearch(); // Focus combo search return; } let targetDataIndex = newIndex === -1 ? this.itemIndexInData(focusedItem.index) - 1 : this.itemIndexInData(newIndex); if (newIndex !== -1) { // If no scroll is required if (this.isScrolledToLast && targetDataIndex === vContainer.state.startIndex) { - // If virt scrollbar is @ bottom, first item is in DOM but not visible + // If virt scrollbar is @ bottom, first item is in DOM but not visible vContainer.scrollTo(targetDataIndex); // This will not trigger `onChunkLoad` super.navigateItem(0); // Focus first visible item } else { @@ -297,6 +239,30 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV } } + /** + * @hidden + */ + protected navigate(direction: Navigate, currentIndex?: number) { + let index = -1; + if (this._focusedItem) { + index = currentIndex ? currentIndex : this._focusedItem.index; + } + const newIndex = this.getNearestSiblingFocusableItemIndex(index, direction); + const vContainer = this.verticalScrollContainer; + const notVirtual = vContainer.dc.instance.notVirtual; + if (notVirtual || !direction) { // If list has no scroll OR no direction is passed + super.navigateItem(newIndex); // use default scroll + } else if (vContainer && vContainer.totalItemCount && vContainer.totalItemCount !== 0) { + this.navigateRemoteItem(direction); + } else { + if (direction === Navigate.Up) { // Navigate UP + this.navigateUp(newIndex); + } else if (direction === Navigate.Down) { // Navigate DOWN + this.navigateDown(newIndex); + } + } + } + private itemIndexInData(index: number) { return this.children.toArray().findIndex(e => e.index === index) + this.verticalScrollContainer.state.startIndex; } @@ -330,59 +296,69 @@ export class IgxComboDropDownComponent extends IgxDropDownBase implements AfterV }); } - protected scrollToHiddenItem(newItem: any): void {} + protected scrollToHiddenItem(newItem: any): void { } /** * @hidden */ protected scrollHandler = () => { - this.disableTransitions = true; + this.comboAPI.disableTransitions = true; } /** * @hidden */ - onToggleOpening(e: CancelableEventArgs) { - const eventArgs = { cancel: false }; - this.onOpening.emit(eventArgs); - e.cancel = eventArgs.cancel; - if (eventArgs.cancel) { - return; - } - this.combo.handleInputChange(); + protected scrollToItem() { } - /** * @hidden */ - onToggleOpened() { - this.combo.triggerCheck(); - this.combo.focusSearchInput(true); - this.onOpened.emit(); + onToggleClosing(e: CancelableEventArgs) { + super.onToggleClosing(e); + this._scrollPosition = this.verticalScrollContainer.getVerticalScroll().scrollTop; } /** * @hidden */ - onToggleClosed() { - this.combo.comboInput.nativeElement.focus(); - this.onClosed.emit(); + updateScrollPosition() { + this.verticalScrollContainer.getVerticalScroll().scrollTop = this._scrollPosition; } /** * @hidden */ - onToggleClosing(e: CancelableEventArgs) { - this.combo.searchValue = ''; - super.onToggleClosing(e); - this._scrollPosition = this.verticalScrollContainer.getVerticalScroll().scrollTop; + public onItemActionKey(key: DropDownActionKey) { + switch (key) { + case DropDownActionKey.ENTER: + this.handleEnter(); + break; + case DropDownActionKey.SPACE: + this.handleSpace(); + break; + case DropDownActionKey.ESCAPE: + this.close(); + } } - /** - * @hidden - */ - updateScrollPosition() { - this.verticalScrollContainer.getVerticalScroll().scrollTop = this._scrollPosition; + private handleEnter() { + if (this.isAddItemFocused()) { + this.combo.addItemToCollection(); + } else { + this.close(); + } + } + + private handleSpace() { + if (this.isAddItemFocused()) { + return; + } else { + this.selectItem(this.focusedItem); + } + } + + private isAddItemFocused(): boolean { + return this.focusedItem instanceof IgxComboAddItemComponent; } public ngAfterViewInit() { diff --git a/projects/igniteui-angular/src/lib/combo/combo-item.component.html b/projects/igniteui-angular/src/lib/combo/combo-item.component.html index 95a0b70bdc7..be352e0c9c3 100644 --- a/projects/igniteui-angular/src/lib/combo/combo-item.component.html +++ b/projects/igniteui-angular/src/lib/combo/combo-item.component.html @@ -1 +1,4 @@ + + + \ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/combo/combo-item.component.ts b/projects/igniteui-angular/src/lib/combo/combo-item.component.ts index f1f29759527..c83ec3a91d0 100644 --- a/projects/igniteui-angular/src/lib/combo/combo-item.component.ts +++ b/projects/igniteui-angular/src/lib/combo/combo-item.component.ts @@ -1,67 +1,92 @@ import { Component, ElementRef, - forwardRef, - HostListener, HostBinding, Inject, - Input + Input, + DoCheck, + Host, + HostListener } from '@angular/core'; +import { IgxDropDownItemComponent } from '../drop-down/drop-down-item.component'; +import { IGX_DROPDOWN_BASE, IDropDownBase } from '../drop-down/drop-down.common'; +import { IgxComboAPIService } from './combo.api'; import { IgxSelectionAPIService } from '../core/selection'; -import { IgxDropDownBase, IgxDropDownItemBase } from '../drop-down/drop-down.base'; -import { IGX_COMBO_COMPONENT, IgxComboBase } from './combo.common'; /** @hidden */ @Component({ selector: 'igx-combo-item', templateUrl: 'combo-item.component.html' }) -export class IgxComboItemComponent extends IgxDropDownItemBase { +export class IgxComboItemComponent extends IgxDropDownItemComponent implements DoCheck { /** * Gets the height of a list item + * @hidden */ + @Input() @HostBinding('style.height.px') - get itemHeight() { - return this.combo.itemHeight; - } + public itemHeight = ''; /** * @hidden */ public get itemID() { - return this.combo.isRemote ? JSON.stringify(this.value) : this.value; + return this.comboAPI.isRemote ? JSON.stringify(this.value) : this.value; } - constructor( - @Inject(IGX_COMBO_COMPONENT) private combo: IgxComboBase, - public dropDown: IgxDropDownBase, - protected elementRef: ElementRef, - protected selection: IgxSelectionAPIService - ) { - super(dropDown, elementRef); + /** + * @hidden + */ + public get comboID() { + return this.comboAPI.comboID; } /** * @hidden + * @internal */ - get isSelected() { - return this.combo.isItemSelected(this.itemID); + public get disableTransitions() { + return this.comboAPI.disableTransitions; + } + + constructor( + protected comboAPI: IgxComboAPIService, + @Inject(IGX_DROPDOWN_BASE) protected dropDown: IDropDownBase, + protected elementRef: ElementRef, + @Inject(IgxSelectionAPIService) protected selection: IgxSelectionAPIService + ) { + super(dropDown, elementRef, selection); } /** * @hidden */ + get isSelected(): boolean { + return this.comboAPI.is_item_selected(this.itemID); + } + + set isSelected(value: boolean) { + if (this.isHeader) { + return; + } + this._isSelected = value; + } + @HostListener('click', ['$event']) clicked(event) { - this.dropDown.disableTransitions = false; + this.comboAPI.disableTransitions = false; if (this.disabled || this.isHeader) { - const focusedItem = this.dropDown.focusedItem; - if (focusedItem) { + const focusedItem = this.dropDown.items.find((item) => item.isFocused); + if (this.dropDown.allowItemsFocus && focusedItem) { focusedItem.element.nativeElement.focus({ preventScroll: true }); } return; } - this.dropDown.selectItem(this, event); + this.dropDown.navigateItem(this.index); + this.comboAPI.set_selected_item(this.itemID, event); + } + + ngDoCheck() { } } diff --git a/projects/igniteui-angular/src/lib/combo/combo.api.ts b/projects/igniteui-angular/src/lib/combo/combo.api.ts new file mode 100644 index 00000000000..1a45aeb8673 --- /dev/null +++ b/projects/igniteui-angular/src/lib/combo/combo.api.ts @@ -0,0 +1,54 @@ +import { IgxComboBase } from './combo.common'; + +/** + * @hidden + */ +export class IgxComboAPIService { + protected combo: IgxComboBase; + + public disableTransitions = false; + + public register(combo: IgxComboBase) { + this.combo = combo; + } + + + public clear(): void { + this.combo = null; + } + + + public get item_focusable(): boolean { + return false; + } + public get isRemote(): boolean { + return this.combo.isRemote; + } + + public add_custom_item(): void { + if (!this.combo) { + return; + } + this.combo.addItemToCollection(); + } + + public get comboID(): string { + return this.combo.id; + } + + public set_selected_item(itemID: any, event?: Event): void { + const selected = this.combo.isItemSelected(itemID); + if (itemID === null || itemID === undefined) { + return; + } + if (!selected) { + this.combo.selectItems([itemID], false, event); + } else { + this.combo.deselectItems([itemID], event); + } + } + + public is_item_selected(itemID: any): boolean { + return this.combo.isItemSelected(itemID); + } +} diff --git a/projects/igniteui-angular/src/lib/combo/combo.common.ts b/projects/igniteui-angular/src/lib/combo/combo.common.ts index 0ed778924ea..dc2fa6d424b 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.common.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.common.ts @@ -1,14 +1,12 @@ -import { ElementRef, EventEmitter, QueryList } from '@angular/core'; +import { ElementRef, EventEmitter } from '@angular/core'; import { CancelableEventArgs } from '../core/utils'; import { IFilteringExpression } from '../data-operations/filtering-expression.interface'; -import { IgxDropDownItemBase } from '../drop-down/drop-down.base'; export const IGX_COMBO_COMPONENT = 'IgxComboComponentToken'; /** @hidden @internal TODO: Evaluate */ export interface IgxComboBase { id: string; - children: QueryList; data: any[]; valueKey: string; groupKey: string; @@ -25,12 +23,12 @@ export interface IgxComboBase { onOpening: EventEmitter; onClosing: EventEmitter; onClosed: EventEmitter; - focusSearchInput(opening?: boolean): void; - triggerCheck(); - setSelectedItem(itemID: any, select?: boolean); - isItemSelected(item: any): boolean; - addItemToCollection(); + triggerCheck(): void; + addItemToCollection(): void; isAddButtonVisible(): boolean; - handleInputChange(event?); + handleInputChange(event?: string): void; + isItemSelected(itemID: any): boolean; + selectItems(itemIDs: any[], clearSelection?: boolean, event?: Event): void; + deselectItems(itemIDs: any[], event?: Event): void; } diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.html b/projects/igniteui-angular/src/lib/combo/combo.component.html index 73c399b21c2..adea9977a1d 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.html +++ b/projects/igniteui-angular/src/lib/combo/combo.component.html @@ -15,19 +15,18 @@ - + clear - - arrow_drop_down - - arrow_drop_up - + + {{ dropdown.collapsed ? 'arrow_drop_down' : 'arrow_drop_up'}} - +
+ [igxDropDownItemNavigation]="dropdown" (focus)="dropdown.onFocus()" [tabindex]="dropdown.collapsed ? -1 : 0" role="listbox" [attr.id]="dropdown.id"> - - - - + @@ -59,12 +55,12 @@
- + - + diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts index 1b71fb70a52..504966080c2 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts @@ -5,7 +5,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { SortingDirection } from '../data-operations/sorting-expression.interface'; import { IgxToggleModule } from '../directives/toggle/toggle.directive'; import { IgxComboItemComponent } from './combo-item.component'; -import { IgxComboComponent, IgxComboModule, IgxComboState } from './combo.component'; +import { IgxComboComponent, IgxComboModule, IgxComboState, IComboSelectionChangeEventArgs } from './combo.component'; import { IgxComboDropDownComponent } from './combo-dropdown.component'; import { FormGroup, FormControl, Validators, FormBuilder, ReactiveFormsModule, FormsModule } from '@angular/forms'; import { IForOfState } from '../directives/for-of/for_of.directive'; @@ -15,7 +15,7 @@ import { UIInteractions, wait } from '../test-utils/ui-interactions.spec'; import { DefaultSortingStrategy } from '../data-operations/sorting-strategy'; import { configureTestSuite } from '../test-utils/configure-suite'; import { IgxDropDownBase } from '../drop-down/drop-down.base'; -import { Navigate } from '../drop-down/drop-down.common'; +import { IgxDropDownItemBase } from '../drop-down/drop-down-item.base'; const CSS_CLASS_COMBO = 'igx-combo'; const CSS_CLASS_COMBO_DROPDOWN = 'igx-combo__drop-down'; @@ -24,7 +24,7 @@ const CSS_CLASS_DROPDOWNLIST = 'igx-drop-down__list'; const CSS_CLASS_CONTENT = 'igx-combo__content'; const CSS_CLASS_CONTAINER = 'igx-display-container'; const CSS_CLASS_DROPDOWNLISTITEM = 'igx-drop-down__item'; -const CSS_CLASS_DROPDOWNBUTTON = 'dropdownToggleButton'; +const CSS_CLASS_DROPDOWNBUTTON = 'igx-combo__clear-button'; const CSS_CLASS_CLEARBUTTON = 'clearButton'; const CSS_CLASS_CHECK_GENERAL = 'igx-combo__checkbox'; const CSS_CLASS_CHECKBOX = 'igx-checkbox'; @@ -78,16 +78,16 @@ describe('igxCombo', () => { const comboButton = fixture.debugElement.query(By.css('button')); expect(fixture.componentInstance).toBeDefined(); expect(combo).toBeDefined(); - expect(combo.dropdown.collapsed).toBeDefined(); + expect(combo.collapsed).toBeDefined(); expect(combo.data).toBeDefined(); - expect(combo.dropdown.collapsed).toBeTruthy(); + expect(combo.collapsed).toBeTruthy(); expect(combo.searchInput).toBeDefined(); expect(comboButton).toBeDefined(); expect(combo.placeholder).toBeDefined(); - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); - expect(combo.dropdown.collapsed).toEqual(false); + expect(combo.collapsed).toEqual(false); expect(combo.searchInput).toBeDefined(); })); it('Should properly return the context (this)', () => { @@ -144,17 +144,10 @@ describe('igxCombo', () => { expect(combo.data.length).toEqual(0); }); it('Combo`s input textbox should be read-only', fakeAsync(() => { - const inputText = 'text'; const fixture = TestBed.createComponent(IgxComboTestComponent); fixture.detectChanges(); const comboElement = fixture.debugElement.query(By.css('input[name=\'comboInput\']')); - const inputElement = comboElement.nativeElement; expect(comboElement.attributes['readonly']).toBeDefined(); - UIInteractions.sendInput(comboElement, inputText, fixture); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - expect(inputElement.value).toEqual(''); })); it('Should properly handle getValueByValueKey calls', () => { const fix = TestBed.createComponent(IgxComboSampleComponent); @@ -196,7 +189,7 @@ describe('igxCombo', () => { const combo = fixture.componentInstance.combo; let headerElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_HEADER)); expect(headerElement).toBeNull(); - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); expect(combo.headerTemplate).toBeDefined(); @@ -213,7 +206,7 @@ describe('igxCombo', () => { const combo = fixture.componentInstance.combo; let footerElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_FOOTER)); expect(footerElement).toBeNull(); - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); expect(combo.footerTemplate).toBeDefined(); @@ -252,8 +245,6 @@ describe('igxCombo', () => { expect(dropdown).toBeDefined(); expect(dropdown.focusedItem).toBeFalsy(); expect(dropdown.verticalScrollContainer).toBeDefined(); - const mockObj = jasmine.createSpyObj('nativeElement', ['focus']); - const mockSearchInput = spyOnProperty(combo, 'searchInput', 'get').and.returnValue({ nativeElement: mockObj }); const mockFn = () => dropdown.navigatePrev(); expect(mockFn).toThrow(); expect(dropdown.focusedItem).toEqual(null); @@ -261,9 +252,9 @@ describe('igxCombo', () => { combo.toggle(); tick(); fix.detectChanges(); - expect(mockObj.focus).toHaveBeenCalledTimes(1); + expect(document.activeElement).toEqual(combo.searchInput.nativeElement); expect(combo.collapsed).toBeFalsy(); - combo.handleKeyUp({ key: 'ArrowDown' }); + combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'ArrowDown'})); fix.detectChanges(); expect(dropdown.focusedItem).toBeTruthy(); expect(dropdown.focusedItem.index).toEqual(0); @@ -272,8 +263,8 @@ describe('igxCombo', () => { dropdown.navigatePrev(); tick(); fix.detectChanges(); - expect(mockObj.focus).toHaveBeenCalledTimes(2); - combo.handleKeyUp({ key: 'ArrowDown' }); + expect(document.activeElement).toEqual(combo.searchInput.nativeElement); + combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'ArrowDown'})); fix.detectChanges(); expect(dropdown.focusedItem).toBeTruthy(); expect(dropdown.focusedItem.index).toEqual(0); @@ -329,7 +320,7 @@ describe('igxCombo', () => { await wait(30); fix.detectChanges(); expect(dropdown.focusedItem).toEqual(lastItem); - dropdown.navigateItem(-1, Navigate.Down); + dropdown.navigateNext(); await wait(30); fix.detectChanges(); expect(virtualMockDOWN).toHaveBeenCalledTimes(1); @@ -340,7 +331,7 @@ describe('igxCombo', () => { expect(dropdown.focusedItem).toEqual(lastItem); fix.detectChanges(); expect(combo.customValueFlag && combo.searchValue !== '').toBeTruthy(); - dropdown.navigateItem(-1, Navigate.Down); + dropdown.navigateNext(); await wait(30); expect(virtualMockDOWN).toHaveBeenCalledTimes(2); lastItem.value = dropdown.verticalScrollContainer.igxForOf[dropdown.verticalScrollContainer.igxForOf.length - 1]; @@ -348,7 +339,7 @@ describe('igxCombo', () => { await wait(30); fix.detectChanges(); expect(dropdown.focusedItem).toEqual(lastItem); - dropdown.navigateItem(-1, Navigate.Down); + dropdown.navigateNext(); expect(virtualMockDOWN).toHaveBeenCalledTimes(3); // TEST move from first item @@ -362,15 +353,15 @@ describe('igxCombo', () => { expect(dropdown.focusedItem).toEqual(firstItem); expect(dropdown.focusedItem.index).toEqual(0); // spyOnProperty(dropdown, 'focusedItem', 'get').and.returnValue(firstItem); - dropdown.navigateItem(-1); + dropdown.navigateFirst(); await wait(30); fix.detectChanges(); expect(virtualMockDOWN).toHaveBeenCalledTimes(3); spyOn(dropdown, 'onBlur').and.callThrough(); - dropdown.navigateItem(-1, Navigate.Up); + dropdown.navigatePrev(); await wait(30); fix.detectChanges(); - expect(virtualMockUP).toHaveBeenCalledTimes(1); + expect(virtualMockUP).toHaveBeenCalledTimes(0); expect(virtualMockDOWN).toHaveBeenCalledTimes(3); })); it('Should call toggle properly', fakeAsync(() => { @@ -383,22 +374,22 @@ describe('igxCombo', () => { spyOn(combo.dropdown, 'onToggleOpened').and.callThrough(); spyOn(combo.dropdown, 'onToggleClosing').and.callThrough(); spyOn(combo.dropdown, 'onToggleClosed').and.callThrough(); - expect(combo.dropdown.collapsed).toEqual(true); - combo.dropdown.toggle(); + expect(combo.collapsed).toEqual(true); + combo.toggle(); tick(); fixture.detectChanges(); expect(combo.dropdown.open).toHaveBeenCalledTimes(1); expect(combo.dropdown.onToggleOpening).toHaveBeenCalledTimes(1); expect(combo.dropdown.onToggleOpened).toHaveBeenCalledTimes(1); - expect(combo.dropdown.collapsed).toEqual(false); + expect(combo.collapsed).toEqual(false); fixture.detectChanges(); - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); expect(combo.dropdown.close).toHaveBeenCalledTimes(1); expect(combo.dropdown.onToggleClosed).toHaveBeenCalledTimes(1); expect(combo.dropdown.onToggleClosing).toHaveBeenCalledTimes(1); - expect(combo.dropdown.collapsed).toEqual(true); + expect(combo.collapsed).toEqual(true); })); it('IgxComboDropDown onFocus and onBlur event', fakeAsync(() => { const fix = TestBed.createComponent(IgxComboSampleComponent); @@ -445,35 +436,39 @@ describe('igxCombo', () => { dropdown.navigateItem(0); fix.detectChanges(); expect(IgxComboDropDownComponent.prototype.navigateItem).toHaveBeenCalledTimes(1); - dropdown.navigateItem(-1, Navigate.Up); + dropdown.navigatePrev(); + expect(IgxComboDropDownComponent.prototype.navigateItem).toHaveBeenCalledTimes(1); + dropdown.navigateItem(dropdown.items.length - 1); + dropdown.navigateNext(); expect(IgxComboDropDownComponent.prototype.navigateItem).toHaveBeenCalledTimes(2); - dropdown.navigateItem(-1, Navigate.Down); - expect(IgxComboDropDownComponent.prototype.navigateItem).toHaveBeenCalledTimes(3); expect(virtualSpyDOWN).toHaveBeenCalled(); - expect(virtualSpyUP).toHaveBeenCalled(); + expect(virtualSpyUP).not.toHaveBeenCalled(); })); - it('Should handle handleKeyDown calls', () => { + it('Should handle handleKeyDown calls', fakeAsync(() => { const fix = TestBed.createComponent(IgxComboSampleComponent); fix.detectChanges(); const combo = fix.componentInstance.combo; + combo.toggle(); + tick(); + fix.detectChanges(); spyOn(combo, 'selectAllItems'); spyOn(combo, 'toggle'); spyOn(combo.dropdown, 'onFocus').and.callThrough(); - combo.handleKeyUp({ key: 'A' }); - combo.handleKeyUp({}); + combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'A'})); + combo.handleKeyUp(new KeyboardEvent('keyup', {})); expect(combo.selectAllItems).toHaveBeenCalledTimes(0); expect(combo.dropdown.onFocus).toHaveBeenCalledTimes(0); - combo.handleKeyUp({ key: 'Enter' }); + combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'Enter'})); expect(combo.selectAllItems).toHaveBeenCalledTimes(0); spyOnProperty(combo, 'filteredData', 'get').and.returnValue([1]); - combo.handleKeyUp({ key: 'Enter' }); + combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'Enter'})); expect(combo.selectAllItems).toHaveBeenCalledTimes(0); - combo.handleKeyUp({ key: 'ArrowDown' }); + combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'ArrowDown'})); expect(combo.selectAllItems).toHaveBeenCalledTimes(0); expect(combo.dropdown.onFocus).toHaveBeenCalledTimes(1); - combo.handleKeyUp({ key: 'Escape' }); + combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'Escape'})); expect(combo.toggle).toHaveBeenCalledTimes(1); - }); + })); it('Dropdown button should open/close dropdown list', fakeAsync(() => { const fixture = TestBed.createComponent(IgxComboTestComponent); fixture.detectChanges(); @@ -482,7 +477,7 @@ describe('igxCombo', () => { comboButton.click(); tick(); fixture.detectChanges(); - expect(combo.dropdown.collapsed).toEqual(false); + expect(combo.collapsed).toEqual(false); const searchInputElement = fixture.debugElement.query(By.css('input[name=\'searchInput\']')).nativeElement; expect(searchInputElement).toBeDefined(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -492,7 +487,7 @@ describe('igxCombo', () => { comboButton.click(); tick(); fixture.detectChanges(); - expect(combo.dropdown.collapsed).toEqual(true); + expect(combo.collapsed).toEqual(true); expect(dropdownList.classList.contains(CSS_CLASS_TOGGLE + '--hidden')).toBeTruthy(); expect(dropdownList.children.length).toEqual(0); })); @@ -515,7 +510,6 @@ describe('igxCombo', () => { const comboInput = combo.comboInput.nativeElement as HTMLElement; expect(comboInput).toBeDefined(); spyOn(combo, 'onArrowDown').and.callThrough(); - spyOn(combo, 'onArrowUp').and.callThrough(); spyOn(combo.dropdown, 'toggle').and.callThrough(); spyOn(combo.dropdown, 'open').and.callThrough(); spyOn(combo.dropdown, 'close').and.callThrough(); @@ -528,14 +522,14 @@ describe('igxCombo', () => { fix.detectChanges(); expect(combo.dropdown.toggle).toHaveBeenCalledTimes(1); - expect(combo.dropdown.collapsed).toEqual(false); + expect(combo.collapsed).toEqual(false); expect(combo.dropdown.open).toHaveBeenCalledTimes(1); - combo.onArrowUp(new KeyboardEvent('keydown', { altKey: false, key: 'ArrowUp' })); + combo.handleKeyDown(new KeyboardEvent('keydown', { altKey: false, key: 'ArrowUp' })); fix.detectChanges(); expect(combo.dropdown.toggle).toHaveBeenCalledTimes(2); expect(combo.dropdown.close).toHaveBeenCalledTimes(1); - combo.onArrowUp(new KeyboardEvent('keydown', { altKey: true, key: 'ArrowUp' })); + combo.handleKeyDown(new KeyboardEvent('keydown', { altKey: true, key: 'ArrowUp' })); fix.detectChanges(); expect(combo.dropdown.toggle).toHaveBeenCalledTimes(3); @@ -578,7 +572,7 @@ describe('igxCombo', () => { const fixture = TestBed.createComponent(IgxComboTestComponent); fixture.detectChanges(); const combo = fixture.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); @@ -883,19 +877,18 @@ describe('igxCombo', () => { }); }); + // dispatchEvent 'Tab' does not trigger default browser behaviour (focus) it('Should properly get the first focusable item when focusing the component list', fakeAsync(() => { const fixture = TestBed.createComponent(IgxComboInputTestComponent); fixture.detectChanges(); const combo = fixture.componentInstance.combo; - spyOn(combo.dropdown, 'getFirstSelectableItem').and.callThrough(); combo.toggle(); tick(); fixture.detectChanges(); - combo.searchInput.nativeElement.dispatchEvent(new KeyboardEvent('keypress', { key: 'Tab'})); - (document.getElementsByClassName('igx-combo__content')[0]).dispatchEvent(new Event('focus')); + combo.dropdown.onFocus(); tick(); fixture.detectChanges(); - expect(combo.dropdown.getFirstSelectableItem).toHaveBeenCalledTimes(1); + (document.getElementsByClassName(CSS_CLASS_CONTENT)[0]).focus(); expect((combo.dropdown.focusedItem.element.nativeElement).textContent.trim()).toEqual('Michigan'); })); }); @@ -962,41 +955,46 @@ describe('igxCombo', () => { const combo = fix.componentInstance.combo; expect(combo.dropdown.items).toBeDefined(); - // items are only accessible when the combo dropdown is opened; - let targetItem: IgxComboItemComponent; - spyOn(combo, 'setSelectedItem').and.callThrough(); - spyOn(combo.dropdown, 'navigateItem').and.callThrough(); - spyOn(combo, 'triggerSelectionChange').and.callThrough(); + spyOn(combo.dropdown, 'selectItem').and.callThrough(); spyOn(combo.dropdown, 'selectedItem').and.callThrough(); spyOn(combo.onSelectionChange, 'emit'); - combo.dropdown.toggle(); + + // items are only accessible when the combo dropdown is opened; + combo.toggle(); tick(); fix.detectChanges(); - expect(combo.dropdown.collapsed).toEqual(false); + expect(combo.collapsed).toEqual(false); expect(combo.dropdown.items.length).toEqual(9); // Virtualization - targetItem = combo.dropdown.items[5] as IgxComboItemComponent; + + let targetItem: IgxDropDownItemBase; + targetItem = combo.dropdown.items[5] as IgxDropDownItemBase; expect(targetItem).toBeDefined(); expect(targetItem.index).toEqual(5); - combo.dropdown.selectItem(targetItem); + combo.dropdown.selectItem(targetItem); fix.detectChanges(); - expect(combo.dropdown.selectedItem).toEqual([targetItem.itemID]); - expect(combo.setSelectedItem).toHaveBeenCalledTimes(1); - expect(combo.setSelectedItem).toHaveBeenCalledWith(targetItem.itemID, true); + expect(combo.selectedItems()).toEqual([targetItem.itemID]); + expect(combo.dropdown.selectItem).toHaveBeenCalledTimes(1); + expect(combo.dropdown.selectItem).toHaveBeenCalledWith(targetItem); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(1); - expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ oldSelection: [], newSelection: [targetItem.itemID] }); + expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: [], + newSelection: [targetItem.itemID], + event: undefined, + cancel: false + }); combo.dropdown.selectItem(targetItem); - expect(combo.dropdown.selectedItem).toEqual([]); - expect(combo.setSelectedItem).toHaveBeenCalledTimes(2); - expect(combo.setSelectedItem).toHaveBeenCalledWith(targetItem.itemID, true); + expect(combo.selectedItems()).toEqual([]); + expect(combo.dropdown.selectItem).toHaveBeenCalledTimes(2); + expect(combo.dropdown.selectItem).toHaveBeenCalledWith(targetItem); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(2); - expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ oldSelection: [targetItem.itemID], newSelection: [] }); - - spyOn(combo, 'addItemToCollection'); - combo.dropdown.selectItem({ value: 'ADD ITEM' } as IgxComboItemComponent, new Event('click')); - fix.detectChanges(); - expect(combo.addItemToCollection).toHaveBeenCalledTimes(1); + expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: [targetItem.itemID], + newSelection: [], + event: undefined, + cancel: false + }); })); it(`Should properly select/deselect items using public methods selectItems and deselectItems`, fakeAsync(() => { const fix = TestBed.createComponent(IgxComboSampleComponent); @@ -1006,14 +1004,19 @@ describe('igxCombo', () => { let oldSelection = []; let newSelection = [combo.data[1], combo.data[5], combo.data[6]]; - combo.dropdown.toggle(); + combo.toggle(); tick(); fix.detectChanges(); combo.selectItems(newSelection); fix.detectChanges(); expect(combo.selectedItems().length).toEqual(newSelection.length); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(1); - expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ oldSelection: oldSelection, newSelection: newSelection }); + expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: oldSelection, + newSelection: newSelection, + event: undefined, + cancel: false + }); let newItem = combo.data[3]; combo.selectItems([newItem]); @@ -1022,7 +1025,12 @@ describe('igxCombo', () => { fix.detectChanges(); expect(combo.selectedItems().length).toEqual(newSelection.length); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(2); - expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ oldSelection: oldSelection, newSelection: newSelection }); + expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: oldSelection, + newSelection: newSelection, + event: undefined, + cancel: false + }); oldSelection = [...newSelection]; newSelection = [combo.data[0]]; @@ -1030,7 +1038,12 @@ describe('igxCombo', () => { fix.detectChanges(); expect(combo.selectedItems().length).toEqual(newSelection.length); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(3); - expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ oldSelection: oldSelection, newSelection: newSelection }); + expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: oldSelection, + newSelection: newSelection, + event: undefined, + cancel: false + }); oldSelection = [...newSelection]; newSelection = []; @@ -1040,7 +1053,12 @@ describe('igxCombo', () => { expect(combo.selectedItems().length).toEqual(newSelection.length); expect(combo.selectedItems().length).toEqual(0); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(4); - expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ oldSelection: oldSelection, newSelection: newSelection }); + expect(combo.onSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: oldSelection, + newSelection: newSelection, + event: undefined, + cancel: false + }); })); it('Should properly select/deselect ALL items', fakeAsync(() => { const fix = TestBed.createComponent(IgxComboSampleComponent); @@ -1052,12 +1070,11 @@ describe('igxCombo', () => { spyOn(combo, 'selectAllItems').and.callThrough(); spyOn(combo, 'deselectAllItems').and.callThrough(); spyOn(combo, 'handleSelectAll').and.callThrough(); - spyOn(combo, 'triggerSelectionChange').and.callThrough(); spyOn(combo.onSelectionChange, 'emit'); - combo.dropdown.toggle(); + combo.toggle(); tick(); fix.detectChanges(); - expect(combo.dropdown.collapsed).toEqual(false); + expect(combo.collapsed).toEqual(false); expect(combo.dropdown.items.length).toEqual(9); // Virtualization combo.handleSelectAll({ checked: true }); @@ -1073,66 +1090,17 @@ describe('igxCombo', () => { expect(combo.deselectAllItems).toHaveBeenCalledTimes(1); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(2); })); - it('Should handle setSelectedItem properly', () => { - const fix = TestBed.createComponent(IgxComboSampleComponent); - fix.detectChanges(); - const combo = fix.componentInstance.combo; - const dropdown = combo.dropdown; - spyOn(dropdown, 'setSelectedItem').and.callThrough(); - spyOn(combo, 'getValueByValueKey').and.callThrough(); - spyOn(combo.onSelectionChange, 'emit').and.callThrough(); - combo.setSelectedItem(null); - expect(combo.selectedItems()).toEqual([]); - dropdown.setSelectedItem(null); - expect(combo.selectedItems()).toEqual([]); - dropdown.setSelectedItem(undefined); - expect(combo.selectedItems()).toEqual([]); - combo.setSelectedItem(undefined); - expect(combo.selectedItems()).toEqual([]); - dropdown.setSelectedItem({ field: 'Connecticut', region: 'New England' }); - expect(combo.selectedItems()).toEqual([{ field: 'Connecticut', region: 'New England' }]); - combo.deselectAllItems(); - expect(combo.selectedItems()).toEqual([]); - combo.setSelectedItem({ field: 'Connecticut', region: 'New England' }); - expect(combo.selectedItems()).toEqual([{ field: 'Connecticut', region: 'New England' }]); - combo.deselectAllItems(); - expect(combo.selectedItems()).toEqual([]); - dropdown.setSelectedItem('Connecticut'); - expect(combo.selectedItems()).toEqual([{ field: 'Connecticut', region: 'New England' }]); - combo.deselectAllItems(); - expect(combo.selectedItems()).toEqual([]); - dropdown.setSelectedItem('Connecticut', false); - expect(combo.selectedItems()).toEqual([]); - combo.deselectAllItems(); - expect(combo.selectedItems()).toEqual([]); - dropdown.setSelectedItem({ field: 'Connecticut', region: 'New England' }, true); - expect(combo.selectedItems()).toEqual([{ field: 'Connecticut', region: 'New England' }]); - spyOn(combo, 'setSelectedItem').and.callThrough(); - const selectionSpy = spyOn(combo, 'triggerSelectionChange').and.callThrough(); - dropdown.setSelectedItem(combo.selectedItems()[0], false); - expect(combo.setSelectedItem).toHaveBeenCalledWith({ field: 'Connecticut', region: 'New England' }, false); - expect(Array.from(selectionSpy.calls.mostRecent().args[0])).toEqual([]); - expect(combo.selectedItems()).toEqual([]); - combo.setSelectedItem('Connecticut', true); - expect(combo.selectedItems()).toEqual([{ field: 'Connecticut', region: 'New England' }]); - expect(combo.selectedItems()[0]).toEqual({ field: 'Connecticut', region: 'New England' }); - combo.setSelectedItem('Connecticut', false); - expect(combo.selectedItems()).toEqual([]); - combo.setSelectedItem('Connecticut', false); - expect(combo.selectedItems()).toEqual([]); - expect(combo.getValueByValueKey).toHaveBeenCalledTimes(5); - expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(13); - }); + it('Should properly return the selected item(s)', () => { const fix = TestBed.createComponent(IgxComboSampleComponent); fix.detectChanges(); const combo = fix.componentInstance.combo; expect(combo.selectedItems()).toEqual([]); - expect(combo.dropdown.selectedItem).toEqual([]); - combo.setSelectedItem('Connecticut'); + expect(combo.selectedItems()).toEqual([]); + combo.selectItems([combo.data[0]]); fix.detectChanges(); - expect(combo.dropdown.selectedItem).toEqual([{ field: 'Connecticut', region: 'New England' }]); - expect(combo.dropdown.selectedItem[0]).toEqual(combo.data[0]); + expect(combo.selectedItems()).toEqual([{ field: 'Connecticut', region: 'New England' }]); + expect(combo.selectedItems()[0]).toEqual(combo.data[0]); }); it('Should append selected items to the input separated by comma', fakeAsync(() => { let expectedOutput: string; @@ -1203,7 +1171,7 @@ describe('igxCombo', () => { const dropdown = combo.dropdown; const input = fixture.debugElement.query(By.css('input[name=\'comboInput\']')); const inputElement = input.nativeElement; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -1259,7 +1227,7 @@ describe('igxCombo', () => { fixture.detectChanges(); const combo = fixture.componentInstance.combo; const inputElement = fixture.debugElement.query(By.css('input[name=\'comboInput\']')).nativeElement; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -1283,12 +1251,14 @@ describe('igxCombo', () => { fixture.debugElement.query(By.css('.' + CSS_CLASS_CLEARBUTTON)).nativeElement.click(); fixture.detectChanges(); tick(); - fixture.detectChanges(); expect(inputElement.value).toEqual(''); + expect(combo.selectedItems().length).toEqual(0); + // Drop down is closed after clicking on clear button, reopen and check boxes + combo.toggle(); + tick(); verifyItemIsUnselected(dropdownList, combo, 3); verifyItemIsUnselected(dropdownList, combo, 7); verifyItemIsUnselected(dropdownList, combo, 1); - expect(combo.selectedItems().length).toEqual(0); })); it('Should show/hide clear button after selecting/deselecting items', fakeAsync(() => { const fixture = TestBed.createComponent(IgxComboTestComponent); @@ -1300,7 +1270,7 @@ describe('igxCombo', () => { expect(fixture.debugElement.queryAll(By.css('.' + CSS_CLASS_CLEARBUTTON)).length).toBeFalsy(); // Open dropdown and select an item - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -1309,13 +1279,13 @@ describe('igxCombo', () => { expect(fixture.debugElement.queryAll(By.css('.' + CSS_CLASS_CLEARBUTTON)).length).toEqual(1); // Close dropdown - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); expect(fixture.debugElement.queryAll(By.css('.' + CSS_CLASS_CLEARBUTTON)).length).toEqual(1); // Open dropdown and deselect an item - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); clickItemCheckbox(dropdownList, 8); @@ -1338,7 +1308,7 @@ describe('igxCombo', () => { expect(fixture.debugElement.queryAll(By.css('.' + CSS_CLASS_CLEARBUTTON)).length).toBeFalsy(); // Close dropdown - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); expect(fixture.debugElement.queryAll(By.css('.' + CSS_CLASS_CLEARBUTTON)).length).toBeFalsy(); @@ -1348,7 +1318,7 @@ describe('igxCombo', () => { fixture.detectChanges(); const combo = fixture.componentInstance.combo; const dropdown = combo.dropdown; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -1376,23 +1346,29 @@ describe('igxCombo', () => { expect(combo.selectedItems()[0]).toEqual(dropdown.items[7].value); expect(combo.selectedItems()[1]).toEqual(dropdown.items[1].value); })); + it('Should trigger onSelectionChange event when selecting/deselecting item', fakeAsync(() => { const fixture = TestBed.createComponent(IgxComboTestComponent); fixture.detectChanges(); const combo = fixture.componentInstance.combo; const dropdown = combo.dropdown; + let timesFired = 1; + const mockEvent = new MouseEvent('click'); const eventParams = { oldSelection: [], - newSelection: [] + newSelection: [], + event: mockEvent, + cancel: false }; - let timesFired = 1; spyOn(combo.onSelectionChange, 'emit').and.callThrough(); - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; const verifyOnSelectionChangeEventIsFired = function (itemIndex: number) { - clickItemCheckbox(dropdownList, itemIndex); + const dropdownItems = fixture.debugElement.queryAll(By.css('.' + CSS_CLASS_DROPDOWNLISTITEM)); + const checkbox = dropdownItems[itemIndex]; + checkbox.triggerEventHandler('click', mockEvent); fixture.detectChanges(); expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(timesFired); expect(combo.onSelectionChange.emit).toHaveBeenCalledWith(eventParams); @@ -1426,7 +1402,7 @@ describe('igxCombo', () => { const fixture = TestBed.createComponent(IgxComboSampleComponent); fixture.detectChanges(); const combo = fixture.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdown = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -1492,7 +1468,7 @@ describe('igxCombo', () => { fixture.detectChanges(); const combo = fixture.componentInstance.combo; const inputElement = fixture.debugElement.query(By.css('input[name=\'comboInput\']')).nativeElement; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -1532,7 +1508,7 @@ describe('igxCombo', () => { fixture.detectChanges(); const combo = fixture.componentInstance.combo; // override selection - fixture.componentInstance.onSelectionChange = (event) => { + fixture.componentInstance.onSelectionChange = (event: IComboSelectionChangeEventArgs) => { event.newSelection = []; }; combo.toggle(); @@ -1592,9 +1568,9 @@ describe('igxCombo', () => { const inputElement = mainInputGroupBundle.children[0]; expect(inputElement.classList.contains('igx-input-group__input')).toBeTruthy(); - expect(inputElement.classList.contains('ng-untouched')).toBeTruthy(); - expect(inputElement.classList.contains('ng-pristine')).toBeTruthy(); - expect(inputElement.classList.contains('ng-valid')).toBeTruthy(); + // expect(inputElement.classList.contains('ng-untouched')).toBeTruthy(); + // expect(inputElement.classList.contains('ng-pristine')).toBeTruthy(); + // expect(inputElement.classList.contains('ng-valid')).toBeTruthy(); expect(inputElement.attributes.getNamedItem('type').nodeValue).toEqual('text'); const dropDownButton = inputGroupBundle.children[1]; @@ -1960,7 +1936,7 @@ describe('igxCombo', () => { return mockScroll(); } expect(mockFunc).toThrow(); - combo.dropdown.toggle(); + combo.toggle(); fix.detectChanges(); expect(combo.dropdown.element).toBeDefined(); expect(mockFunc).toBeDefined(); @@ -2195,7 +2171,7 @@ describe('igxCombo', () => { const fixture = TestBed.createComponent(IgxComboEmptyTestComponent); fixture.detectChanges(); const combo = fixture.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const dropdownList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWNLIST)).nativeElement; @@ -2228,7 +2204,7 @@ describe('igxCombo', () => { const fix = TestBed.createComponent(IgxComboInputTestComponent); fix.detectChanges(); const combo = fix.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fix.detectChanges(); expect(combo.groupKey).toEqual('region'); @@ -2249,7 +2225,7 @@ describe('igxCombo', () => { const fix = TestBed.createComponent(IgxComboInputTestComponent); fix.detectChanges(); const combo = fix.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fix.detectChanges(); expect(combo.groupKey).toEqual('region'); @@ -2270,6 +2246,7 @@ describe('igxCombo', () => { fix.detectChanges(); expect(combo.dropdown.items[0].value).toEqual(combo.data[0]); })); + it('Should properly handle click events on Disabled / Header items', fakeAsync(() => { const fix = TestBed.createComponent(IgxComboSampleComponent); fix.detectChanges(); @@ -2281,16 +2258,16 @@ describe('igxCombo', () => { expect(combo.collapsed).toBeFalsy(); expect(combo.dropdown.headers).toBeDefined(); expect(combo.dropdown.headers.length).toEqual(2); - combo.dropdown.headers[0].clicked({}); + combo.dropdown.headers[0].clicked(null); fix.detectChanges(); const mockObj = jasmine.createSpyObj('nativeElement', ['focus']); spyOnProperty(combo.dropdown, 'focusedItem', 'get').and.returnValue({ element: { nativeElement: mockObj } }); - combo.dropdown.headers[0].clicked({}); + combo.dropdown.headers[0].clicked(null); fix.detectChanges(); - expect(mockObj.focus).toHaveBeenCalled(); + expect(mockObj.focus).not.toHaveBeenCalled(); // Focus only if `allowItemFocus === true` - combo.dropdown.items[0].clicked({}); + combo.dropdown.items[0].clicked(null); fix.detectChanges(); expect(document.activeElement).toEqual(combo.searchInput.nativeElement); })); @@ -2446,12 +2423,12 @@ describe('igxCombo', () => { expect(combo.filter).toHaveBeenCalledTimes(1); expect(combo.onSearchInput.emit).toHaveBeenCalledTimes(0); - combo.handleInputChange({ key: 'Fake' }); + combo.handleInputChange('Fake'); fix.detectChanges(); expect(combo.filter).toHaveBeenCalledTimes(2); expect(combo.onSearchInput.emit).toHaveBeenCalledTimes(1); - expect(combo.onSearchInput.emit).toHaveBeenCalledWith({ key: 'Fake' }); + expect(combo.onSearchInput.emit).toHaveBeenCalledWith('Fake'); combo.handleInputChange(''); fix.detectChanges(); @@ -2554,7 +2531,7 @@ describe('igxCombo', () => { const combo = fixture.componentInstance.combo; const expectedValues = combo.data.filter(data => data.includes('P')); - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const searchInput = fixture.debugElement.query(By.css('input[name=\'searchInput\']')); @@ -2644,7 +2621,7 @@ describe('igxCombo', () => { const fixture = TestBed.createComponent(IgxComboTestComponent); fixture.detectChanges(); const combo = fixture.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const searchInput = fixture.debugElement.query(By.css('input[name=\'searchInput\']')); @@ -2676,7 +2653,7 @@ describe('igxCombo', () => { const fixture = TestBed.createComponent(IgxComboTestComponent); fixture.detectChanges(); const combo = fixture.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); @@ -2692,7 +2669,7 @@ describe('igxCombo', () => { searchInputElement.dispatchEvent(event); tick(); fixture.detectChanges(); - expect(combo.dropdown.collapsed).toBeTruthy(); + expect(combo.collapsed).toBeTruthy(); expect(searchInputElement.textContent).toEqual(''); })); it('Group header should not be visible when no results are filtered for a group', fakeAsync(() => { @@ -2707,7 +2684,7 @@ describe('igxCombo', () => { } return filteredArray; }, {}); - combo.dropdown.toggle(); + combo.toggle(); tick(); fixture.detectChanges(); const searchInput = fixture.debugElement.query(By.css('input[name=\'searchInput\']')); @@ -2736,14 +2713,10 @@ describe('igxCombo', () => { expect(combo.value).toEqual(''); expect(combo.comboInput.nativeElement.value).toEqual(''); - const triggerSelectionSpy = spyOn(combo, 'triggerSelectionChange').and.callThrough(); - const valueSetterSpy = spyOnProperty(combo, 'value', 'set').and.callThrough(); combo.selectItems([component.items[0], component.items[1]]); fix.detectChanges(); tick(); fix.detectChanges(); - expect(valueSetterSpy).toHaveBeenCalled(); - expect(triggerSelectionSpy).toHaveBeenCalled(); expect(combo.comboInput.nativeElement.value).toEqual(component.items[0].field + ', ' + component.items[1].field); expect(combo.value).toEqual(component.items[0].field + ', ' + component.items[1].field); expect(combo.selectedItems()).toEqual([component.items[0], component.items[1]]); @@ -2794,20 +2767,18 @@ describe('igxCombo', () => { fix.detectChanges(); let addItem; const combo = fix.componentInstance.combo; - combo.dropdown.toggle(); + combo.toggle(); tick(); fix.detectChanges(); addItem = fix.debugElement.query(By.css('.igx-combo__add')); expect(addItem).toEqual(null); - expect(combo.children.length).toBeTruthy(); UIInteractions.sendInput(combo.searchInput, 'New', fix); tick(); fix.detectChanges(); expect(combo.searchValue).toEqual('New'); addItem = fix.debugElement.query(By.css('.igx-combo__add')); expect(addItem === null).toBeFalsy(); - expect(combo.children.length).toBeTruthy(); UIInteractions.sendInput(combo.searchInput, 'New York', fix); tick(); @@ -2816,8 +2787,8 @@ describe('igxCombo', () => { fix.detectChanges(); addItem = fix.debugElement.query(By.css('.igx-combo__add')); expect(addItem).toEqual(null); - expect(combo.children.length).toBeTruthy(); })); + it(`Should handle enter keydown on "Add Item" properly`, fakeAsync(() => { const fixture = TestBed.createComponent(IgxComboSampleComponent); fixture.detectChanges(); @@ -2827,7 +2798,7 @@ describe('igxCombo', () => { tick(); fixture.detectChanges(); - spyOnProperty(combo, 'searchValue', 'get').and.returnValue('My New Custom Item'); + combo.searchValue = 'My New Custom Item'; combo.handleInputChange(); tick(); @@ -2837,7 +2808,7 @@ describe('igxCombo', () => { tick(); expect(combo.isAddButtonVisible()).toBeTruthy(); - const dropdownHandler = document.getElementsByClassName('igx-combo__content')[0] as HTMLElement; + let dropdownHandler = document.getElementsByClassName('igx-combo__content')[0] as HTMLElement; combo.handleKeyUp(new KeyboardEvent('keyup', { key: 'ArrowDown' })); tick(); @@ -2846,9 +2817,11 @@ describe('igxCombo', () => { tick(); fixture.detectChanges(); - // SPACE does not add item to collection expect(combo.collapsed).toBeFalsy(); expect(combo.value).toEqual(''); + expect(combo.isAddButtonVisible()).toBeTruthy(); + + dropdownHandler = document.getElementsByClassName('igx-combo__content')[0] as HTMLElement; dropdownHandler.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); tick(); @@ -2856,6 +2829,7 @@ describe('igxCombo', () => { expect(combo.collapsed).toBeFalsy(); expect(combo.value).toEqual('My New Custom Item'); })); + it(`Should handle click on "Add Item" properly`, fakeAsync(() => { const fixture = TestBed.createComponent(IgxComboSampleComponent); fixture.detectChanges(); @@ -2865,7 +2839,7 @@ describe('igxCombo', () => { tick(); fixture.detectChanges(); - spyOnProperty(combo, 'searchValue', 'get').and.returnValue('My New Custom Item'); + combo.searchValue = 'My New Custom Item'; combo.handleInputChange(); tick(); @@ -2935,6 +2909,7 @@ describe('igxCombo', () => { tick(); expect(combo.dropdown.items.length).toBeGreaterThan(0); })); + it(`Should properly display "Add Item" button when filtering is off`, fakeAsync(() => { const fixture = TestBed.createComponent(IgxComboInContainerTestComponent); fixture.detectChanges(); @@ -2962,7 +2937,8 @@ describe('igxCombo', () => { }); describe('Form control tests: ', () => { - it('Should properly initialize when used as a form control', fakeAsync(() => { + + it('Should properly initialize when used as a form control', fakeAsync(() => { const fix = TestBed.createComponent(IgxComboFormComponent); fix.detectChanges(); const combo = fix.componentInstance.combo; @@ -2980,6 +2956,7 @@ describe('igxCombo', () => { combo.selectItems([combo.dropdown.items[0], combo.dropdown.items[1]]); expect(combo.valid).toEqual(IgxComboState.VALID); })); + it('Should be possible to be enabled/disabled when used as a form control', () => { const fix = TestBed.createComponent(IgxComboFormComponent); fix.detectChanges(); @@ -3221,7 +3198,7 @@ class IgxComboSampleComponent { this.initData = this.items; } - onSelectionChange(ev) { + onSelectionChange(ev: IComboSelectionChangeEventArgs) { } } diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.ts b/projects/igniteui-angular/src/lib/combo/combo.component.ts index 38954b03cd6..1d1b7d26059 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.ts @@ -1,10 +1,8 @@ import { ConnectedPositioningStrategy } from './../services/overlay/position/connected-positioning-strategy'; import { CommonModule } from '@angular/common'; import { - AfterViewInit, ChangeDetectorRef, Component, ContentChild, - ElementRef, EventEmitter, - HostBinding, HostListener, Input, NgModule, OnInit, OnDestroy, Output, QueryList, - TemplateRef, ViewChild, ViewChildren, Optional, Self, Inject, Directive + AfterViewInit, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostBinding, HostListener, + Input, NgModule, OnInit, OnDestroy, Output, TemplateRef, ViewChild, Optional, Self, Inject, ViewChildren, QueryList } from '@angular/core'; import { IgxComboItemDirective, @@ -15,29 +13,31 @@ import { IgxComboAddItemDirective } from './combo.directives'; import { FormsModule, ReactiveFormsModule, ControlValueAccessor, NgControl } from '@angular/forms'; -import { IgxCheckboxComponent, IgxCheckboxModule } from '../checkbox/checkbox.component'; +import { IgxCheckboxModule } from '../checkbox/checkbox.component'; import { IgxSelectionAPIService } from '../core/selection'; import { cloneArray, CancelableEventArgs } from '../core/utils'; import { IgxStringFilteringOperand, IgxBooleanFilteringOperand } from '../data-operations/filtering-condition'; import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface'; import { SortingDirection, ISortingExpression } from '../data-operations/sorting-expression.interface'; import { IgxForOfModule, IForOfState } from '../directives/for-of/for_of.directive'; +import { IgxIconModule } from '../icon/index'; import { IgxRippleModule } from '../directives/ripple/ripple.directive'; import { IgxToggleModule } from '../directives/toggle/toggle.directive'; import { IgxButtonModule } from '../directives/button/button.directive'; import { IgxDropDownModule } from '../drop-down/drop-down.component'; -import { IgxIconModule } from '../icon/index'; import { IgxInputGroupModule } from '../input-group/input-group.component'; import { IgxComboItemComponent } from './combo-item.component'; import { IgxComboDropDownComponent } from './combo-dropdown.component'; import { IgxComboFilterConditionPipe, IgxComboFilteringPipe, IgxComboGroupingPipe, IgxComboSortingPipe } from './combo.pipes'; import { OverlaySettings, AbsoluteScrollStrategy } from '../services'; -import { Subscription } from 'rxjs'; +import { Subject } from 'rxjs'; import { DeprecateProperty } from '../core/deprecateDecorators'; import { DefaultSortingStrategy, ISortingStrategy } from '../data-operations/sorting-strategy'; import { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from '../core/density'; -import { IGX_COMBO_COMPONENT } from './combo.common'; -import { IgxDropDownItemBase } from '../drop-down/drop-down.base'; +import { IGX_COMBO_COMPONENT, IgxComboBase } from './combo.common'; +import { takeUntil } from 'rxjs/operators'; +import { IgxComboAddItemComponent } from './combo-add-item.component'; +import { IgxComboAPIService } from './combo.api'; /** Custom strategy to provide the combo with callback on initial positioning */ class ComboConnectedPositionStrategy extends ConnectedPositioningStrategy { @@ -80,7 +80,7 @@ export enum IgxComboState { INVALID } -export interface IComboSelectionChangeEventArgs { +export interface IComboSelectionChangeEventArgs extends CancelableEventArgs { oldSelection: any[]; newSelection: any[]; event?: Event; @@ -98,9 +98,9 @@ const noop = () => { }; @Component({ selector: 'igx-combo', templateUrl: 'combo.component.html', - providers: [{ provide: IGX_COMBO_COMPONENT, useExisting: IgxComboComponent }] + providers: [{ provide: IGX_COMBO_COMPONENT, useExisting: IgxComboComponent }, IgxComboAPIService] }) -export class IgxComboComponent extends DisplayDensityBase implements AfterViewInit, ControlValueAccessor, OnInit, OnDestroy { +export class IgxComboComponent extends DisplayDensityBase implements IgxComboBase, AfterViewInit, ControlValueAccessor, OnInit, OnDestroy { /** * @hidden */ @@ -124,39 +124,23 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - protected _filteringExpressions: IFilteringExpression [] = []; + protected _filteringExpressions: IFilteringExpression[] = []; /** * @hidden */ - protected _sortingExpressions: ISortingExpression [] = []; + protected _sortingExpressions: ISortingExpression[] = []; /** * @hidden */ protected _groupKey = ''; - /** - * @hidden - */ - protected _valueKey = ''; /** * @hidden */ protected _displayKey: string; - private _addItemTemplate: TemplateRef; - private _emptyTemplate: TemplateRef; - private _footerTemplate: TemplateRef; - private _headerTemplate: TemplateRef; - private _headerItemTemplate: TemplateRef; - private _itemTemplate: TemplateRef; private _dataType = ''; + private destroy$ = new Subject(); private _data = []; private _filteredData = []; - private _children: QueryList; - private _dropdownContainer: ElementRef = null; - private _searchInput: ElementRef = null; - private _comboInput: ElementRef = null; - private _valid = IgxComboState.INITIAL; - private _statusChanges$: Subscription; - private _width = '100%'; private _positionCallback: () => void; private _onChangeCallback: (_: any) => void = noop; private overlaySettings: OverlaySettings = { @@ -164,22 +148,21 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn modal: false, closeOnOutsideClick: true }; - private _value = ''; - private _searchValue = ''; - constructor( protected elementRef: ElementRef, protected cdr: ChangeDetectorRef, protected selection: IgxSelectionAPIService, + protected comboAPI: IgxComboAPIService, @Self() @Optional() public ngControl: NgControl, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions) { - super(_displayDensityOptions); + super(_displayDensityOptions); if (this.ngControl) { // Note: we provide the value accessor through here, instead of // the `providers` to avoid running into a circular import. this.ngControl.valueAccessor = this; } + this.comboAPI.register(this); } /** @@ -188,33 +171,17 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn @ViewChild(IgxComboDropDownComponent, { read: IgxComboDropDownComponent }) public dropdown: IgxComboDropDownComponent; - /** - * @hidden - */ - @ViewChild('selectAllCheckbox', { read: IgxCheckboxComponent }) - public selectAllCheckbox: IgxCheckboxComponent; - - /** - * @hidden - */ - get searchInput() { - return this._searchInput; - } - /** * @hidden */ @ViewChild('searchInput') - set searchInput(content: ElementRef) { - this._searchInput = content; - } + public searchInput: ElementRef = null; /** * @hidden */ - get comboInput() { - return this._comboInput; - } + @ViewChild('comboInput') + public comboInput: ElementRef = null; /** * @hidden @@ -223,68 +190,23 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn return this.filterable || this.allowCustomValues; } - /** - * @hidden - */ - @ViewChild('comboInput') - set comboInput(content: ElementRef) { - this._comboInput = content; - } - @ContentChild(IgxComboItemDirective, { read: TemplateRef }) - public set itemTemplate(val: TemplateRef) { - this._itemTemplate = val; - } - - public get itemTemplate(): TemplateRef { - return this._itemTemplate; - } + public itemTemplate: TemplateRef = null; @ContentChild(IgxComboHeaderDirective, { read: TemplateRef }) - public set headerTemplate(val: TemplateRef) { - this._headerTemplate = val; - } - - public get headerTemplate(): TemplateRef { - return this._headerTemplate; - } + public headerTemplate: TemplateRef = null; @ContentChild(IgxComboFooterDirective, { read: TemplateRef }) - public set footerTemplate(val: TemplateRef) { - this._footerTemplate = val; - } - - public get footerTemplate(): TemplateRef { - return this._footerTemplate; - } + public footerTemplate: TemplateRef = null; @ContentChild(IgxComboHeaderItemDirective, { read: TemplateRef }) - public set headerItemTemplate(val: TemplateRef) { - this._headerItemTemplate = val; - } - - public get headerItemTemplate(): TemplateRef { - return this._headerItemTemplate; - } - + public headerItemTemplate: TemplateRef = null; @ContentChild(IgxComboAddItemDirective, { read: TemplateRef }) - public set addItemTemplate(val: TemplateRef) { - this._addItemTemplate = val; - } - - public get addItemTemplate(): TemplateRef { - return this._addItemTemplate; - } + public addItemTemplate: TemplateRef = null; @ContentChild(IgxComboEmptyDirective, { read: TemplateRef }) - public set emptyTemplate(val: TemplateRef) { - this._emptyTemplate = val; - } - - public get emptyTemplate(): TemplateRef { - return this._emptyTemplate; - } + public emptyTemplate: TemplateRef = null; /** * @hidden */ @@ -373,31 +295,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * @hidden */ @ViewChild('dropdownItemContainer') - protected set dropdownContainer(val: ElementRef) { - this._dropdownContainer = val; - } - - /** - * @hidden - */ - protected get dropdownContainer(): ElementRef { - return this._dropdownContainer; - } - - /** - * @hidden - */ - @ViewChildren(IgxComboItemComponent, { read: IgxComboItemComponent }) - public set children(list: QueryList) { - this._children = list; - } - - /** - * @hidden - */ - public get children(): QueryList { - return this._children; - } + protected dropdownContainer: ElementRef = null; /** * Emitted when item selection is changing, before the selection completes @@ -427,7 +325,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * ``` */ @Output() - public onOpened = new EventEmitter(); + public onOpened = new EventEmitter(); /** * Emitted before the dropdown is closed @@ -447,7 +345,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * ``` */ @Output() - public onClosed = new EventEmitter(); + public onClosed = new EventEmitter(); /** * Emitted when an item is being added to the data collection @@ -511,20 +409,14 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn */ @HostBinding('style.width') @Input() - public get width() { - return this._width; - } - - public set width(val) { - this._width = val; - } + public width = '100%'; /** * @hidden */ @HostBinding('class.igx-input-group--valid') public get validClass(): boolean { - return this._valid === IgxComboState.VALID; + return this.valid === IgxComboState.VALID; } /** @@ -532,7 +424,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn */ @HostBinding('class.igx-input-group--invalid') public get invalidClass(): boolean { - return this._valid === IgxComboState.INVALID; + return this.valid === IgxComboState.INVALID; } /** @@ -551,7 +443,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * @hidden */ @HostBinding('attr.aria-expanded') - public get ariaExpanded() { + public get ariaExpanded(): boolean { return !this.dropdown.collapsed; } @@ -681,7 +573,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * ``` */ @Input() - get data() { + get data(): any[] { return this._data; } set data(val: any[]) { @@ -702,12 +594,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * ``` */ @Input() - get valueKey() { - return this._valueKey; - } - set valueKey(val: string) { - this._valueKey = val; - } + public valueKey: string; @Input() set displayKey(val: string) { @@ -732,7 +619,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * ``` */ get displayKey() { - return this._displayKey ? this._displayKey : this._valueKey; + return this._displayKey ? this._displayKey : this.valueKey; } /** @@ -799,18 +686,38 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn @Input() public type = 'box'; + /** + * Gets/Sets if control is valid, when used in a form + * + * ```typescript + * // get + * let valid = this.combo.valid; + * ``` + * ```typescript + * // set + * this.combo.valid = IgxComboState.INVALID; + * ``` + */ + public valid: IgxComboState = IgxComboState.INITIAL; + /** * @hidden */ - public onBlur(event) { + public searchValue = ''; + + /** + * @hidden + */ + @HostListener('blur') + public onBlur(): void { if (this.dropdown.collapsed) { - this._valid = IgxComboState.INITIAL; + this.valid = IgxComboState.INITIAL; if (this.ngControl) { if (!this.ngControl.valid) { - this._valid = IgxComboState.INVALID; + this.valid = IgxComboState.INVALID; } } else if (this._hasValidators() && !this.elementRef.nativeElement.checkValidity()) { - this._valid = IgxComboState.INVALID; + this.valid = IgxComboState.INVALID; } } } @@ -827,9 +734,9 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn */ @HostListener('keydown.ArrowDown', ['$event']) @HostListener('keydown.Alt.ArrowDown', ['$event']) - onArrowDown(evt) { - evt.preventDefault(); - evt.stopPropagation(); + onArrowDown(event: Event) { + event.preventDefault(); + event.stopPropagation(); if (this.dropdown.collapsed) { this.toggle(); } @@ -838,22 +745,9 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - // @HostListener('keydown.ArrowUp', ['$event']) - // @HostListener('keydown.Alt.ArrowUp', ['$event']) - onArrowUp(evt) { - evt.preventDefault(); - evt.stopPropagation(); - if (!this.dropdown.collapsed) { - this.toggle(); - } - } - - /** - * @hidden - */ - onInputClick(evt) { - evt.stopPropagation(); - evt.preventDefault(); + onInputClick(event: Event) { + event.stopPropagation(); + event.preventDefault(); this.toggle(); } @@ -876,7 +770,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * this.combo.virtualizationState(state); * ``` */ - set virtualizationState(state) { + set virtualizationState(state: IForOfState) { this.dropdown.verticalScrollContainer.state = state; } @@ -888,7 +782,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * let count = this.combo.totalItemCount; * ``` */ - get totalItemCount() { + get totalItemCount(): number { return this.dropdown.verticalScrollContainer.totalItemCount; } /** @@ -899,35 +793,11 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * this.combo.totalItemCount(remoteService.count); * ``` */ - set totalItemCount(count) { + set totalItemCount(count: number) { this.dropdown.verticalScrollContainer.totalItemCount = count; this.cdr.detectChanges(); } - /** - * Gets if control is valid, when used in a form - * - * ```typescript - * // get - * let valid = this.combo.valid; - * ``` - */ - public get valid(): IgxComboState { - return this._valid; - } - - /** - * Sets valid state of the combo - * - * ```typescript - * // get - * this.combo.valid(IgxComboState.INVALID); - * ``` - */ - public set valid(value: IgxComboState) { - this._valid = value; - } - /** * @hidden */ @@ -938,14 +808,14 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - public get filteringExpressions(): IFilteringExpression [] { + public get filteringExpressions(): IFilteringExpression[] { return this.filterable ? this._filteringExpressions : []; } /** * @hidden */ - public set filteringExpressions(value: IFilteringExpression []) { + public set filteringExpressions(value: IFilteringExpression[]) { this._filteringExpressions = value; this.cdr.markForCheck(); } @@ -953,14 +823,14 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - public get sortingExpressions(): ISortingExpression [] { + public get sortingExpressions(): ISortingExpression[] { return this._sortingExpressions; } /** * @hidden */ - public set sortingExpressions(value: ISortingExpression []) { + public set sortingExpressions(value: ISortingExpression[]) { this._sortingExpressions = value; this.cdr.markForCheck(); } @@ -982,7 +852,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn } /** - * Combo value + * The text displayed in the combo input * * ```typescript * // get @@ -992,31 +862,6 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn get value(): string { return this._value; } - /** - * Combo value - * - * ```html - * - * - * ``` - */ - set value(val) { - this._value = val; - } - - /** - * @hidden - */ - get searchValue() { - return this._searchValue; - } - - /** - * @hidden - */ - set searchValue(val: string) { - this._searchValue = val; - } /** * @hidden @@ -1036,12 +881,11 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - public handleKeyUp(evt) { - if (evt.key === 'ArrowDown' || evt.key === 'Down') { - this.dropdownContainer.nativeElement.focus(); - this.dropdown.onFocus(); + public handleKeyUp(event: KeyboardEvent): void { + if (event.key === 'ArrowDown' || event.key === 'Down') { this.dropdown.focusedItem = this.dropdown.items[0]; - } else if (evt.key === 'Escape' || evt.key === 'Esc') { + this.dropdownContainer.nativeElement.focus(); + } else if (event.key === 'Escape' || event.key === 'Esc') { this.toggle(); } } @@ -1049,25 +893,30 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - public handleKeyDown(evt) { - if (evt.key === 'ArrowUp' || evt.key === 'Up') { - this.onArrowUp(evt); + public handleKeyDown(event: KeyboardEvent) { + if (event.key === 'ArrowUp' || event.key === 'Up') { + event.preventDefault(); + event.stopPropagation(); + if (!this.dropdown.collapsed) { + this.toggle(); + } } } - private checkMatch() { - this.customValueFlag = this.displayKey ? - !this.filteredData - .some((e) => (e[this.displayKey]).toString().toLowerCase() === this.searchValue.trim().toLowerCase()) && - this.allowCustomValues : - !this.filteredData - .some((e) => e.toString().toLowerCase() === this.searchValue.trim().toLowerCase()) && this.allowCustomValues; + private checkMatch(): void { + const displayKey = this.displayKey; + const matchFn = (e) => { + const value = displayKey ? e[displayKey] : e; + return value.toString().toLowerCase() === this.searchValue.trim().toLowerCase(); + }; + const itemMatch = this.filteredData.some(matchFn); + this.customValueFlag = this.allowCustomValues && !itemMatch; } /** * @hidden */ - public handleInputChange(event?) { + public handleInputChange(event?: string) { if (event !== undefined) { this.dropdown.verticalScrollContainer.scrollTo(0); this.onSearchInput.emit(event); @@ -1083,7 +932,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * @hidden */ public sort(fieldName: string, dir: SortingDirection = SortingDirection.Asc, ignoreCase: boolean = true, - strategy: ISortingStrategy = DefaultSortingStrategy.instance()): void { + strategy: ISortingStrategy = DefaultSortingStrategy.instance()): void { if (!fieldName) { return; } @@ -1108,8 +957,8 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - protected prepare_sorting_expression(state: ISortingExpression [], fieldName: string, dir: SortingDirection, ignoreCase: boolean, - strategy: ISortingStrategy) { + protected prepare_sorting_expression(state: ISortingExpression[], fieldName: string, dir: SortingDirection, ignoreCase: boolean, + strategy: ISortingStrategy) { if (dir === SortingDirection.None) { state.splice(state.findIndex((expr) => expr.fieldName === fieldName), 1); @@ -1144,7 +993,12 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn this.dataType === DataTypes.COMPLEX; } - private _stringifyItemID(itemID) { + /** + * If the data source is remote, returns JSON.stringify(itemID) + * @hidden + * @internal + */ + private _stringifyItemID(itemID: any) { return this.isRemote && typeof itemID === 'object' ? JSON.stringify(itemID) : itemID; } @@ -1152,70 +1006,15 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn return this.isRemote && typeof itemID === 'string' ? JSON.parse(itemID) : itemID; } - private changeSelectedItem(newItem: any, select?: boolean) { - if (!newItem && newItem !== 0) { - return; - } - const newSelection = select ? - this.selection.add_item(this.id, newItem) : - this.selection.delete_item(this.id, newItem); - this.triggerSelectionChange(newSelection); - } - /** + * Returns if the specified itemID is selected * @hidden + * @internal */ - public setSelectedItem(itemID: any, select = true) { - if (itemID === undefined || itemID === null) { - return; - } - const newItem = this.dropdown.items.find((item) => item.itemID === itemID); - if (newItem) { - if (newItem.disabled || newItem.isHeader) { - return; - } - if (!newItem.isSelected) { - this.changeSelectedItem(itemID, true); - } else { - this.changeSelectedItem(itemID, false); - } - } else { - const target = typeof itemID === 'object' ? itemID : this.getValueByValueKey(itemID); - if (target) { - this.changeSelectedItem(target, select); - } - } - } - - /** - * @hidden - */ - public isItemSelected(item) { + public isItemSelected(item: any): boolean { return this.selection.is_item_selected(this.id, this._stringifyItemID(item)); } - /** - * @hidden - */ - protected triggerSelectionChange(newSelectionAsSet: Set) { - const oldSelection = this.dropdown.selectedItem; - const newSelection = newSelectionAsSet ? Array.from(newSelectionAsSet) : []; - if (oldSelection !== newSelection) { - const args: IComboSelectionChangeEventArgs = { oldSelection, newSelection }; - this.onSelectionChange.emit(args); - newSelectionAsSet = this.selection.get_empty(); - for (let i = 0; i < args.newSelection.length; i++) { - newSelectionAsSet.add(args.newSelection[i]); - } - this.selection.set(this.id, newSelectionAsSet); - this.value = this.dataType !== DataTypes.PRIMITIVE ? - newSelection.map((id) => this._parseItemID(id)[this.displayKey]).join(', ') : - newSelection.join(', '); - // this.isHeaderChecked(); - this._onChangeCallback(newSelection); - } - } - /** * @hidden */ @@ -1255,7 +1054,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn [this.displayKey]: newValue } : newValue; if (this.groupKey) { - Object.assign(addedItem, { [this.groupKey] : this.defaultFallbackGroup}); + Object.assign(addedItem, { [this.groupKey]: this.defaultFallbackGroup }); } const oldCollection = this.data; const newCollection = [...this.data]; @@ -1268,14 +1067,15 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn // If you mutate the array, no pipe is invoked and the display isn't updated; // if you replace the array, the pipe executes and the display is updated. this.data = cloneArray(this.data); - this.changeSelectedItem(addedItem, true); + this.selectItems([addedItem], false); this.customValueFlag = false; this.searchInput.nativeElement.focus(); + this.dropdown.focusedItem = null; this.handleInputChange(); } /** - * @hidden; + * @hidden */ public focusSearchInput(opening?: boolean): void { if (this.displaySearchInput && this.searchInput) { @@ -1283,7 +1083,6 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn } else { if (opening) { this.dropdownContainer.nativeElement.focus(); - this.dropdown.onFocus(); } else { this.comboInput.nativeElement.focus(); this.toggle(); @@ -1325,7 +1124,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn protected onStatusChanged() { if ((this.ngControl.control.touched || this.ngControl.control.dirty) && (this.ngControl.control.validator || this.ngControl.control.asyncValidator)) { - this._valid = this.ngControl.valid ? IgxComboState.VALID : IgxComboState.INVALID; + this.valid = this.ngControl.valid ? IgxComboState.VALID : IgxComboState.INVALID; } } @@ -1344,9 +1143,9 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn this._positionCallback = () => this.dropdown.updateScrollPosition(); this.overlaySettings.positionStrategy = new ComboConnectedPositionStrategy(this._positionCallback); this.overlaySettings.positionStrategy.settings.target = this.elementRef.nativeElement; - + this.selection.set(this.id, new Set()); if (this.ngControl && this.ngControl.value) { - this.triggerSelectionChange(this.ngControl.value); + this.selectItems(this.ngControl.value, true); } } @@ -1357,7 +1156,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn this.filteredData = [...this.data]; if (this.ngControl) { - this._statusChanges$ = this.ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this)); + this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged.bind(this)); } } @@ -1365,9 +1164,9 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * @hidden */ public ngOnDestroy() { - if (this._statusChanges$) { - this._statusChanges$.unsubscribe(); - } + this.destroy$.complete(); + this.comboAPI.clear(); + this.selection.clear(this.id); } /** @@ -1431,8 +1230,8 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn /** * @hidden */ - public handleClearItems(event) { - this.deselectAllItems(true); + public handleClearItems(event: Event): void { + this.deselectAllItems(true, event); event.stopPropagation(); } @@ -1444,7 +1243,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * *``` */ - public toggle() { + public toggle(): void { this.dropdown.toggle(this.overlaySettings); } @@ -1456,7 +1255,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * *``` */ - public open() { + public open(): void { this.dropdown.open(this.overlaySettings); } @@ -1468,7 +1267,7 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * *``` */ - public close() { + public close(): void { this.dropdown.close(); } @@ -1476,11 +1275,10 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * Gets drop down state. * * ```typescript - * // get * let state = this.combo.collapsed; * ``` */ - public get collapsed() { + public get collapsed(): boolean { return this.dropdown.collapsed; } @@ -1488,12 +1286,11 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * Get current selection state * @returns Array of selected items * ```typescript - * // get * let selectedItems = this.combo.selectedItems(); * ``` */ public selectedItems() { - const items = this.dropdown.selectedItem; + const items = Array.from(this.selection.get(this.id)); return this.isRemote ? items.map(item => this._parseItemID(item)) : items; } @@ -1502,14 +1299,13 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * @param newItems new items to be selected * @param clearCurrentSelection if true clear previous selected items * ```typescript - * // get * this.combo.selectItems(["New York", "New Jersey"]); * ``` */ - public selectItems(newItems: Array, clearCurrentSelection?: boolean) { + public selectItems(newItems: Array, clearCurrentSelection?: boolean, event?: Event) { if (newItems) { const newSelection = this.selection.add_items(this.id, newItems, clearCurrentSelection); - this.triggerSelectionChange(newSelection); + this.setSelection(newSelection, event); } } @@ -1517,14 +1313,13 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * Deselect defined items * @param items items to deselected * ```typescript - * // get * this.combo.deselectItems(["New York", "New Jersey"]); * ``` */ - public deselectItems(items: Array) { + public deselectItems(items: Array, event?: Event) { if (items) { const newSelection = this.selection.delete_items(this.id, items); - this.triggerSelectionChange(newSelection); + this.setSelection(newSelection, event); } } @@ -1532,29 +1327,113 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn * Select all (filtered) items * @param ignoreFilter if set to true, selects all items, otherwise selects only the filtered ones. * ```typescript - * // get * this.combo.selectAllItems(); * ``` */ - public selectAllItems(ignoreFilter?: boolean) { + public selectAllItems(ignoreFilter?: boolean, event?: Event) { const allVisible = this.selection.get_all_ids(ignoreFilter ? this.data : this.filteredData); const newSelection = this.selection.add_items(this.id, allVisible); - this.triggerSelectionChange(newSelection); + this.setSelection(newSelection, event); } /** * Deselect all (filtered) items * @param ignoreFilter if set to true, deselects all items, otherwise deselects only the filtered ones. * ```typescript - * // get * this.combo.deselectAllItems(); * ``` */ - public deselectAllItems(ignoreFilter?: boolean) { - const newSelection = this.filteredData.length === this.data.length || ignoreFilter ? - this.selection.get_empty() : - this.selection.delete_items(this.id, this.selection.get_all_ids(this.filteredData)); - this.triggerSelectionChange(newSelection); + public deselectAllItems(ignoreFilter?: boolean, event?: Event): void { + let newSelection = this.selection.get_empty(); + if (this.filteredData.length !== this.data.length && !ignoreFilter) { + newSelection = this.selection.delete_items(this.id, this.selection.get_all_ids(this.filteredData)); + } + this.setSelection(newSelection, event); + } + + /** + * Selects/Deselects an item using it's valueKey value + * @param itemID the valueKey of the specified item + * @param select If the item should be selected (true) or deselcted (false) + * + * ```typescript + * items: { field: string, region: string}[] = data; + * this.combo.setSelectedItem('Connecticut', true); + * // combo.valueKey === 'field' + * // items[n] === { field: 'Connecticut', state: 'New England'} + * ``` + */ + public setSelectedItem(itemID: any, select = true, event?: Event): void { + if (itemID === null || itemID === undefined) { + return; + } + const itemValue = this.getValueByValueKey(itemID); + if (itemValue !== null && itemValue !== undefined) { + if (select) { + this.selectItems([itemValue], false, event); + } else { + this.deselectItems([itemValue], event); + } + } + } + + protected setSelection(newSelection: Set, event?: Event): void { + const oldSelectionEmit = Array.from(this.selection.get(this.id) || []); + const newSelectionEmit = Array.from(newSelection || []); + const args: IComboSelectionChangeEventArgs = { + newSelection: newSelectionEmit, + oldSelection: oldSelectionEmit, + event, + cancel: false + }; + this.onSelectionChange.emit(args); + if (!args.cancel) { + this.selection.select_items(this.id, args.newSelection, true); + this._value = this.dataType !== DataTypes.PRIMITIVE ? + args.newSelection.map((id) => this._parseItemID(id)[this.displayKey]).join(', ') : + args.newSelection.join(', '); + this._onChangeCallback(args.newSelection); + } + } + /** + * Event handlers + * @hidden + * @internal + */ + public handleOpening(event: CancelableEventArgs) { + this.onOpening.emit(event); + if (event.cancel) { + return; + } + this.handleInputChange(); + } + + /** + * @hidden + */ + public handleOpened() { + this.triggerCheck(); + this.focusSearchInput(true); + this.onOpened.emit(); + } + + /** + * @hidden + */ + public handleClosing(event) { + this.onClosing.emit(event); + if (event.cancel) { + return; + } + this.searchValue = ''; + } + + /** + * @hidden + */ + public handleClosed() { + this.comboInput.nativeElement.focus(); + this.onClosed.emit(); } } @@ -1563,14 +1442,14 @@ export class IgxComboComponent extends DisplayDensityBase implements AfterViewIn */ @NgModule({ declarations: [IgxComboComponent, IgxComboItemComponent, IgxComboFilterConditionPipe, IgxComboGroupingPipe, - IgxComboFilteringPipe, IgxComboSortingPipe, IgxComboDropDownComponent, + IgxComboFilteringPipe, IgxComboSortingPipe, IgxComboDropDownComponent, IgxComboAddItemComponent, IgxComboItemDirective, IgxComboEmptyDirective, IgxComboHeaderItemDirective, IgxComboHeaderDirective, IgxComboFooterDirective, IgxComboAddItemDirective], - exports: [IgxComboComponent, IgxComboItemComponent, IgxComboDropDownComponent, + exports: [IgxComboComponent, IgxComboItemComponent, IgxComboDropDownComponent, IgxComboAddItemComponent, IgxComboItemDirective, IgxComboEmptyDirective, IgxComboHeaderItemDirective, diff --git a/projects/igniteui-angular/src/lib/combo/index.ts b/projects/igniteui-angular/src/lib/combo/index.ts new file mode 100644 index 00000000000..d7f63d39226 --- /dev/null +++ b/projects/igniteui-angular/src/lib/combo/index.ts @@ -0,0 +1 @@ +export * from './combo.component'; diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down-item.base.ts b/projects/igniteui-angular/src/lib/drop-down/drop-down-item.base.ts new file mode 100644 index 00000000000..4bd9704f814 --- /dev/null +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down-item.base.ts @@ -0,0 +1,195 @@ +import { IDropDownBase, IGX_DROPDOWN_BASE } from './drop-down.common'; +import { Input, HostBinding, HostListener, ElementRef, Optional, Inject, DoCheck } from '@angular/core'; +import { IgxSelectionAPIService } from '../core/selection'; + +/** + * An abstract class defining a drop-down item: + * With properties / styles for selection, highlight, height + * Bindable property for passing data (`value: any`) + * Parent component (has to be used under a parent with type `IDropDownBase`) + * Method for handling click on Host() + */ +export abstract class IgxDropDownItemBase implements DoCheck { + /** + * @hidden + */ + protected _isFocused = false; + protected _isSelected = false; + + /** + * @hidden + */ + public get itemID() { + return this; + } + + /** + * Gets/sets the value of the item if the item is databound + * + * ```typescript + * // usage in IgxDropDownItemComponent + * // get + * let mySelectedItemValue = this.dropdown.selectedItem.value; + * + * // set + * let mySelectedItem = this.dropdown.selectedItem; + * mySelectedItem.value = { id: 123, name: 'Example Name' } + * + * // usage in IgxComboItemComponent + * // get + * let myComboItemValue = this.combo.items[0].value; + * ``` + */ + @Input() + public value: any; + + /** + * @hidden + */ + @HostBinding('class.igx-drop-down__item') + get itemStyle(): boolean { + return !this.isHeader; + } + + /** + * Sets/Gets if the item is the currently selected one in the dropdown + * + * ```typescript + * let mySelectedItem = this.dropdown.selectedItem; + * let isMyItemSelected = mySelectedItem.isSelected; // true + * ``` + */ + @Input() + get isSelected(): boolean { + return this._isSelected; + } + + set isSelected(value: boolean) { + if (this.isHeader) { + return; + } + this._isSelected = value; + } + + /** + * @hidden + */ + @HostBinding('attr.aria-selected') + @HostBinding('class.igx-drop-down__item--selected') + get selectedStyle(): boolean { + return this.isSelected; + } + + /** + * Sets/gets if the given item is focused + * ```typescript + * let mySelectedItem = this.dropdown.selectedItem; + * let isMyItemFocused = mySelectedItem.isFocused; + * ``` + */ + @HostBinding('class.igx-drop-down__item--focused') + get isFocused(): boolean { + return (!this.isHeader && !this.disabled) && this._isFocused; + } + + /** + * ```html + * + *
+ * {{item.field}} + *
+ *
+ * ``` + */ + set isFocused(value: boolean) { + this._isFocused = value; + } + + /** + * Sets/gets if the given item is header + * ```typescript + * // get + * let mySelectedItem = this.dropdown.selectedItem; + * let isMyItemHeader = mySelectedItem.isHeader; + * ``` + * + * ```html + * + * + *
+ * {{item.field}} +*
+ * + * ``` + */ + @Input() + @HostBinding('class.igx-drop-down__header') + public isHeader = false; + + /** + * Sets/gets if the given item is disabled + * + * ```typescript + * // get + * let mySelectedItem = this.dropdown.selectedItem; + * let myItemIsDisabled = mySelectedItem.disabled; + * ``` + * + * ```html + * + *
+ * {{item.field}} + *
+ *
+ * ``` + */ + @Input() + @HostBinding('class.igx-drop-down__item--disabled') + public disabled = false; + + /** + * Gets item index + * @hidden + */ + public get index(): number { + return this.dropDown.items.indexOf(this); + } + + /** + * Gets item element height + * @hidden + */ + public get elementHeight(): number { + return this.elementRef.nativeElement.clientHeight; + } + + /** + * Get item html element + * @hidden + */ + public get element(): ElementRef { + return this.elementRef; + } + + constructor( + @Inject(IGX_DROPDOWN_BASE) protected dropDown: IDropDownBase, + protected elementRef: ElementRef, + @Optional() @Inject(IgxSelectionAPIService) protected selection?: IgxSelectionAPIService + ) { } + + /** + * @hidden + */ + @HostListener('click', ['$event']) + clicked(event) { + } + + ngDoCheck(): void { + if (this.isSelected) { + const dropDownSelectedItem = this.selection.first_item(this.dropDown.id); + if (!dropDownSelectedItem || this !== dropDownSelectedItem) { + this.dropDown.selectItem(this); + } + } + } +} diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down-item.component.ts b/projects/igniteui-angular/src/lib/drop-down/drop-down-item.component.ts index 62cdd7665c8..150b665f332 100644 --- a/projects/igniteui-angular/src/lib/drop-down/drop-down-item.component.ts +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down-item.component.ts @@ -1,11 +1,16 @@ import { Component, - ElementRef, Input, - DoCheck + DoCheck, + HostListener, + HostBinding } from '@angular/core'; -import { IgxDropDownBase, IgxDropDownItemBase } from './drop-down.base'; +import { IgxDropDownItemBase } from './drop-down-item.base'; +/** + * The `` is a container intended for row items in + * a `` container. + */ @Component({ selector: 'igx-drop-down-item', templateUrl: 'drop-down-item.component.html' @@ -14,41 +19,30 @@ export class IgxDropDownItemComponent extends IgxDropDownItemBase implements DoC /** * @hidden */ - protected _isSelected = false; - - constructor( - public dropDown: IgxDropDownBase, - protected elementRef: ElementRef - ) { - super(dropDown, elementRef); + @HostBinding('attr.tabindex') + get setTabIndex() { + const shouldSetTabIndex = this.dropDown.allowItemsFocus && !(this.disabled || this.isHeader); + if (shouldSetTabIndex) { + return 0; + } else { + return null; + } } /** - * Sets/Gets if the item is the currently selected one in the dropdown - * - * ```typescript - * let mySelectedItem = this.dropdown.selectedItem; - * let isMyItemSelected = mySelectedItem.isSelected; // true - * ``` + * @hidden */ - get isSelected() { - return this._isSelected; - } - @Input() - set isSelected(value: boolean) { - if (this.isHeader) { + @HostListener('click', ['$event']) + clicked(event) { + if (this.disabled || this.isHeader) { + const focusedItem = this.dropDown.items.find((item) => item.isFocused); + if (this.dropDown.allowItemsFocus && focusedItem) { + focusedItem.element.nativeElement.focus({ preventScroll: true }); + } return; } - - this._isSelected = value; - } - - ngDoCheck(): void { - if (this.isSelected) { - const dropDownSelectedItem = this.dropDown.selectedItem; - if (!dropDownSelectedItem || this !== dropDownSelectedItem) { - this.dropDown.selectItem(this); - } + if (this.selection) { + this.dropDown.selectItem(this, event); } } } diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down-navigation.directive.ts b/projects/igniteui-angular/src/lib/drop-down/drop-down-navigation.directive.ts new file mode 100644 index 00000000000..cf15819d31e --- /dev/null +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down-navigation.directive.ts @@ -0,0 +1,138 @@ +import { Directive, Optional, Self, Input, HostListener, Inject } from '@angular/core'; +import { IGX_DROPDOWN_BASE } from './drop-down.common'; +import { IDropDownNavigationDirective } from './drop-down.common'; +import { IgxDropDownBase } from './drop-down.base'; + +/** Key actions that have designated handlers in IgxDropDownComponent */ +export enum DropDownActionKey { + ESCAPE = 'escape', + ENTER = 'enter', + SPACE = 'space' +} +/** + * Navigation Directive that handles keyboard events on its host and controls a targeted IgxDropDownBase component + */ +@Directive({ + selector: '[igxDropDownItemNavigation]' +}) +export class IgxDropDownItemNavigationDirective implements IDropDownNavigationDirective { + + protected _target: IgxDropDownBase = null; + + constructor(@Self() @Optional() @Inject(IGX_DROPDOWN_BASE) public dropdown: IgxDropDownBase) { } + + /** + * Gets the target of the navigation directive; + * + * ```typescript + * // Get + * export class MyComponent { + * ... + * @ContentChild(IgxDropDownNavigationDirective) + * navDirective: IgxDropDownNavigationDirective = null + * ... + * const navTarget: IgxDropDownBase = navDirective.navTarget + * } + * ``` + */ + get target(): IgxDropDownBase { + return this._target; + } + + /** + * Sets the target of the navigation directive; + * If no valid target is passed, it falls back to the drop down context + * + * ```html + * + * + * ... + * + * ... + * + * ``` + */ + @Input('igxDropDownItemNavigation') + set target(target: IgxDropDownBase) { + this._target = target ? target : this.dropdown; + } + + /** + * Captures keydown events and calls the appropriate handlers on the target component + */ + @HostListener('keydown', ['$event']) + handleKeyDown(event: KeyboardEvent) { + if (event) { + const key = event.key.toLowerCase(); + if (!this.target.collapsed) { // If dropdown is opened + const navKeys = ['esc', 'escape', 'enter', 'space', 'spacebar', ' ', + 'arrowup', 'up', 'arrowdown', 'down', 'home', 'end']; + if (navKeys.indexOf(key) === -1) { // If key has appropriate function in DD + return; + } + event.preventDefault(); + event.stopPropagation(); + } else { // If dropdown is closed, do nothing + return; + } + switch (key) { + case 'esc': + case 'escape': + this.target.onItemActionKey(DropDownActionKey.ESCAPE, event); + break; + case 'enter': + this.target.onItemActionKey(DropDownActionKey.ENTER, event); + break; + case 'space': + case 'spacebar': + case ' ': + this.target.onItemActionKey(DropDownActionKey.SPACE, event); + break; + case 'arrowup': + case 'up': + this.onArrowUpKeyDown(); + break; + case 'arrowdown': + case 'down': + this.onArrowDownKeyDown(); + break; + case 'home': + this.onHomeKeyDown(); + break; + case 'end': + this.onEndKeyDown(); + break; + default: + return; + } + } + } + + /** + * Navigates to previous item + */ + onArrowDownKeyDown() { + this.target.navigateNext(); + } + + /** + * Navigates to previous item + */ + onArrowUpKeyDown() { + this.target.navigatePrev(); + } + + /** + * Navigates to target's last item + */ + onEndKeyDown() { + this.target.navigateLast(); + } + + /** + * Navigates to target's first item + */ + onHomeKeyDown() { + this.target.navigateFirst(); + } +} diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down.base.ts b/projects/igniteui-angular/src/lib/drop-down/drop-down.base.ts index 4eae00d0b80..9f1aa80c71e 100644 --- a/projects/igniteui-angular/src/lib/drop-down/drop-down.base.ts +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down.base.ts @@ -1,29 +1,34 @@ import { - Input, HostBinding, HostListener, ElementRef, OnInit, - QueryList, ViewChild, Output, EventEmitter, ChangeDetectorRef + Input, HostBinding, ElementRef, QueryList, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; -import { CancelableEventArgs } from '../core/utils'; -import { IgxSelectionAPIService } from '../core/selection'; -import { OverlaySettings } from '../services'; -import { IToggleView } from '../core/navigation'; -import { IgxToggleDirective } from '../directives/toggle/toggle.directive'; -import { ISelectionEventArgs, Navigate } from './drop-down.common'; +import { Navigate, ISelectionEventArgs } from './drop-down.common'; +import { IDropDownList } from './drop-down.common'; +import { DropDownActionKey } from './drop-down-navigation.directive'; +import { IgxDropDownItemBase } from './drop-down-item.base'; let NEXT_ID = 0; -/** @hidden TODO: refactor */ -export abstract class IgxDropDownBase implements OnInit, IToggleView { - private _initiallySelectedItem: IgxDropDownItemBase = null; +/** + * An abstract class, defining a drop-down component, with: + * Properties for display styles and classes + * A collection items of type `IgxDropDownItemBase` + * Properties and methods for navigating (highlighting/focusing) items from the collection + * Properties and methods for selecting items from the collection + */ +export abstract class IgxDropDownBase implements IDropDownList { + protected _width; + protected _height; protected _focusedItem: any = null; - private _width; - private _height; - private _id = `igx-drop-down-${NEXT_ID++}`; - + protected _id = `igx-drop-down-${NEXT_ID++}`; protected children: QueryList; - @ViewChild(IgxToggleDirective) - protected toggleDirective: IgxToggleDirective; + /** + * Get dropdown's html element of it scroll container + */ + protected get scrollContainer() { + return this.element; + } /** * Emitted when item selection is changing, before the selection completes @@ -36,144 +41,49 @@ export abstract class IgxDropDownBase implements OnInit, IToggleView { public onSelection = new EventEmitter(); /** - * Emitted before the dropdown is opened - * - * ```html - * - * ``` - */ - @Output() - public onOpening = new EventEmitter(); - - /** - * Emitted after the dropdown is opened - * - * ```html - * - * ``` - */ - @Output() - public onOpened = new EventEmitter(); - - /** - * Emitted before the dropdown is closed - * - * ```html - * - * ``` - */ - @Output() - public onClosing = new EventEmitter(); - - /** - * Emitted after the dropdown is closed - * - * ```html - * - * ``` - */ - @Output() - public onClosed = new EventEmitter(); - - /** - * Gets the width of the drop down + * Gets/Sets the width of the drop down * * ```typescript * // get * let myDropDownCurrentWidth = this.dropdown.width; * ``` - */ - @Input() - get width() { - return this._width; - } - /** - * Sets the width of the drop down - * * ```html * * * ``` */ - set width(value) { - this._width = value; - this.toggleDirective.element.style.width = value; - } + @Input() + public width: string; /** - * Gets the height of the drop down + * Gets/Sets the height of the drop down * * ```typescript * // get * let myDropDownCurrentHeight = this.dropdown.height; * ``` - */ - @Input() - get height() { - return this._height; - } - /** - * Sets the height of the drop down - * * ```html * * * ``` */ - set height(value) { - this._height = value; - this.toggleDirective.element.style.height = value; - } - - /** - * Gets/sets whether items take focus. Disabled by default. - * When enabled, drop down items gain tab index and are focused when active - - * this includes activating the selected item when opening the drop down and moving with keyboard navigation. - * - * Note: Keep that focus shift in mind when using the igxDropDownItemNavigation directive - * and ensure it's placed either on each focusable item or a common ancestor to allow it to handle keyboard events. - * - * ```typescript - * // get - * let dropDownAllowsItemFocus = this.dropdown.allowItemsFocus; - * ``` - * - * ```html - * - * - * ``` - */ @Input() - public allowItemsFocus = false; + public height: string; /** - * Gets the drop down's id + * Gets/Sets the drop down's id * * ```typescript * // get * let myDropDownCurrentId = this.dropdown.id; * ``` - */ - @Input() - get id(): string { - return this._id; - } - /** - * Sets the drop down's id - * * ```html * * * ``` */ - set id(value: string) { - this.selection.set(value, this.selection.get(this.id)); - this._id = value; - this.toggleDirective.id = value; - } - - @HostBinding('class.igx-drop-down') - cssClass = 'igx-drop-down'; + @Input() + public id: string; /** * Gets/Sets the drop down's container max height. @@ -182,43 +92,20 @@ export abstract class IgxDropDownBase implements OnInit, IToggleView { * // get * let maxHeight = this.dropdown.maxHeight; * ``` - * * ```html * * * ``` */ @Input() + @HostBinding('style.maxHeight') public maxHeight = null; /** - * Gets if the dropdown is collapsed - * - * ```typescript - * let isCollapsed = this.dropdown.collapsed; - * ``` - */ - public get collapsed(): boolean { - return this.toggleDirective.collapsed; - } - - /** - * Get currently selected item - * - * ```typescript - * let currentItem = this.dropdown.selectedItem; - * ``` + * @hidden */ - public get selectedItem(): any { - const selectedItem = this.selection.first_item(this.id); - if (selectedItem) { - if (selectedItem.isSelected) { - return selectedItem; - } - this.selection.clear(this.id); - } - return null; - } + @HostBinding('class.igx-drop-down') + public cssClass = true; /** * Get all non-header items @@ -272,75 +159,39 @@ export abstract class IgxDropDownBase implements OnInit, IToggleView { } /** - * @hidden - * @internal - */ - public disableTransitions = false; - - /** - * Get dropdown's html element of it scroll container + * Gets if the dropdown is collapsed */ - protected get scrollContainer() { - return this.toggleDirective.element; + public get collapsed(): boolean { + return false; } constructor( protected elementRef: ElementRef, - protected cdr: ChangeDetectorRef, - protected selection: IgxSelectionAPIService) { } - - /** - * Select an item by index - * @param index of the item to select - */ - setSelectedItem(index: number) { - if (index < 0 || index >= this.items.length) { - return; + protected cdr: ChangeDetectorRef) { } + + /** Keydown Handler */ + public onItemActionKey(key: DropDownActionKey, event?: Event) { + switch (key) { + case DropDownActionKey.ENTER: + case DropDownActionKey.SPACE: + this.selectItem(this.focusedItem, event); + break; + case DropDownActionKey.ESCAPE: } - - const newSelection = this.items.find((item) => item.index === index); - if (newSelection.isHeader) { - return; - } - - this.changeSelectedItem(newSelection); } /** - * Opens the dropdown - * - * ```typescript - * this.dropdown.open(); - * ``` - */ - open(overlaySettings?: OverlaySettings) { - this.toggleDirective.open(overlaySettings); - } - - /** - * Closes the dropdown - * - * ```typescript - * this.dropdown.close(); - * ``` - */ - close() { - this.toggleDirective.close(); - } - - /** - * Toggles the dropdown - * - * ```typescript - * this.dropdown.toggle(); - * ``` + * Emits onSelection with the target item & event + * @hidden + * @param newSelection the item selected + * @param event the event that triggered the call */ - toggle(overlaySettings?: OverlaySettings) { - if (this.toggleDirective.collapsed) { - this.open(overlaySettings); - } else { - this.close(); - } + public selectItem(newSelection?: IgxDropDownItemBase, event?: Event) { + this.onSelection.emit({ + newSelection, + oldSelection: null, + cancel: false + }); } /** @@ -366,176 +217,18 @@ export abstract class IgxDropDownBase implements OnInit, IToggleView { index = currentIndex ? currentIndex : this._focusedItem.index; } const newIndex = this.getNearestSiblingFocusableItemIndex(index, direction); - this.navigateItem(newIndex, direction); - } - - /** - * @hidden - */ - navigateFirst() { - this.navigate(Navigate.Down, -1); - } - - /** - * @hidden - */ - navigateLast() { - this.navigate(Navigate.Up, this.items.length); - } - - /** - * @hidden - */ - navigateNext() { - this.navigate(Navigate.Down); - } - - /** - * @hidden - */ - navigatePrev() { - this.navigate(Navigate.Up); - } - - /** - * @hidden - */ - ngOnInit() { - this.toggleDirective.id = this.id; - this.selection.clear(this.id); - } - - - /** - * @hidden - */ - onToggleOpening(e: CancelableEventArgs) { - const eventArgs = { cancel: false }; - this.onOpening.emit(eventArgs); - e.cancel = eventArgs.cancel; - if (eventArgs.cancel) { - return; - } - this.scrollToItem(this.selectedItem); - } - - /** - * @hidden - */ - onToggleOpened() { - this._initiallySelectedItem = this.selectedItem; - this._focusedItem = this.selectedItem; - if (this._focusedItem) { - this._focusedItem.isFocused = true; - } else if (this.allowItemsFocus) { - const firstItemIndex = this.getNearestSiblingFocusableItemIndex(-1, Navigate.Down); - if (firstItemIndex !== -1) { - this.navigateItem(firstItemIndex); - } - } - this.onOpened.emit(); - } - - /** - * @hidden - */ - onToggleClosing(e: CancelableEventArgs) { - const eventArgs = { cancel: false }; - this.onClosing.emit(eventArgs); - e.cancel = eventArgs.cancel; - } - - /** - * @hidden - */ - onToggleClosed() { - if (this._focusedItem) { - this._focusedItem.isFocused = false; - } - - this.onClosed.emit(); - } - - /** - * @hidden - */ - protected scrollToItem(item: IgxDropDownItemBase) { - const itemPosition = this.calculateScrollPosition(item); - this.scrollContainer.scrollTop = (itemPosition); - } - - protected scrollToHiddenItem(newItem: IgxDropDownItemBase) { - const elementRect = newItem.element.nativeElement.getBoundingClientRect(); - const parentRect = this.scrollContainer.getBoundingClientRect(); - if (parentRect.top > elementRect.top) { - this.scrollContainer.scrollTop -= (parentRect.top - elementRect.top); - } - - if (parentRect.bottom < elementRect.bottom) { - this.scrollContainer.scrollTop += (elementRect.bottom - parentRect.bottom); - } + this.navigateItem(newIndex); } - /** - * @hidden - */ - public selectItem(item: IgxDropDownItemBase, event?) { - if (item === null) { - return; - } - this.changeSelectedItem(item); - - if (event) { - this.toggleDirective.close(); - } - } - - /** - * @hidden - */ - protected changeSelectedItem(newSelection?: IgxDropDownItemBase): boolean { - const oldSelection = this.selectedItem; - if (!newSelection) { - newSelection = this._focusedItem; - } - - const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false }; - this.onSelection.emit(args); - if (!args.cancel) { - this.selection.set(this.id, new Set([newSelection])); - } - - return !args.cancel; - } - - /** - * @hidden - */ - protected calculateScrollPosition(item: IgxDropDownItemBase): number { - if (!item) { - return 0; - } - - const elementRect = item.element.nativeElement.getBoundingClientRect(); - const parentRect = this.scrollContainer.getBoundingClientRect(); - const scrollDelta = parentRect.top - elementRect.top; - let scrollPosition = this.scrollContainer.scrollTop - scrollDelta; - - const dropDownHeight = this.scrollContainer.clientHeight; - scrollPosition -= dropDownHeight / 2; - scrollPosition += item.elementHeight / 2; - - return Math.floor(scrollPosition); - } - - private getNearestSiblingFocusableItemIndex(startIndex: number, direction: Navigate): number { + protected getNearestSiblingFocusableItemIndex(startIndex: number, direction: Navigate): number { let index = startIndex; - while (this.items[index + direction] && this.items[index + direction].disabled) { + const items = this.items; + while (items[index + direction] && items[index + direction].disabled) { index += direction; } index += direction; - if (index >= 0 && index < this.items.length) { + if (index >= 0 && index < items.length) { return index; } else { return -1; @@ -543,10 +236,10 @@ export abstract class IgxDropDownBase implements OnInit, IToggleView { } /** - * @hidden - * @internal + * Navigates to the item on the specified index + * @param newIndex number - the index of the item in the `items` collection */ - public navigateItem(newIndex: number, direction?: Navigate) { + public navigateItem(newIndex: number) { if (newIndex !== -1) { const oldItem = this._focusedItem; const newItem = this.items[newIndex]; @@ -558,220 +251,44 @@ export abstract class IgxDropDownBase implements OnInit, IToggleView { this._focusedItem.isFocused = true; } } - /** - * @hidden - */ - public getFirstSelectableItem() { - return this.children.find(child => !child.isHeader && !child.disabled); - } -} - -/** - * The `` is a container intended for row items in - * a `` container. - */ - -export abstract class IgxDropDownItemBase { - - /** - * @hidden - */ - protected _isFocused = false; /** * @hidden */ - public get itemID() { - return; - } - - /** - * Gets/sets the value of the item if the item is databound - * - * ```typescript - * // usage in IgxDropDownItemComponent - * // get - * let mySelectedItemValue = this.dropdown.selectedItem.value; - * - * // set - * let mySelectedItem = this.dropdown.selectedItem; - * mySelectedItem.value = { id: 123, name: 'Example Name' } - * - * // usage in IgxComboItemComponent - * // get - * let myComboItemValue = this.combo.items[0].value; - * ``` - */ - @Input() - public value: any; - - /** - * @hidden - */ - @HostBinding('class.igx-drop-down__item') - get itemStyle(): boolean { - return !this.isHeader; - } - - /** - * Gets if the item is the currently selected one in the dropdown - * - * ```typescript - * let mySelectedItem = this.dropdown.selectedItem; - * let isMyItemSelected = mySelectedItem.isSelected; // true - * ``` - */ - get isSelected() { - return this.dropDown.selectedItem === this; - } - - /** - * @hidden - */ - @HostBinding('attr.aria-selected') - @HostBinding('class.igx-drop-down__item--selected') - get selectedStyle(): boolean { - return this.isSelected; - } - - /** - * Sets/gets if the given item is focused - * ```typescript - * let mySelectedItem = this.dropdown.selectedItem; - * let isMyItemFocused = mySelectedItem.isFocused; - * ``` - */ - @HostBinding('class.igx-drop-down__item--focused') - get isFocused() { - return this._isFocused; - } - - /** - * ```html - * - *
- * {{item.field}} - *
- *
- * ``` - */ - set isFocused(value: boolean) { - if (this.disabled || this.isHeader) { - this._isFocused = false; - return; - } - - if (this.dropDown.allowItemsFocus && value && !this.dropDown.collapsed) { - this.elementRef.nativeElement.focus({ preventScroll: true }); - } - this._isFocused = value; - } - - /** - * Sets/gets if the given item is header - * ```typescript - * // get - * let mySelectedItem = this.dropdown.selectedItem; - * let isMyItemHeader = mySelectedItem.isHeader; - * ``` - * - * ```html - * - * - *
- * {{item.field}} -*
- *
- * ``` - */ - @Input() - @HostBinding('class.igx-drop-down__header') - public isHeader = false; - - /** - * Sets/gets if the given item is disabled - * - * ```typescript - * // get - * let mySelectedItem = this.dropdown.selectedItem; - * let myItemIsDisabled = mySelectedItem.disabled; - * ``` - * - * ```html - * - *
- * {{item.field}} - *
- *
- * ``` - */ - @Input() - @HostBinding('class.igx-drop-down__item--disabled') - public disabled = false; - - /** - * @hidden - */ - @HostBinding('attr.tabindex') - get setTabIndex() { - const shouldSetTabIndex = this.dropDown.allowItemsFocus && !(this.disabled || this.isHeader); - if (shouldSetTabIndex) { - return 0; - } else { - return null; - } + public navigateFirst() { + this.navigate(Navigate.Down, -1); } /** - * Gets item index * @hidden */ - public get index(): number { - return this.dropDown.items.indexOf(this); + public navigateLast() { + this.navigate(Navigate.Up, this.items.length); } /** - * Gets item element height * @hidden */ - public get elementHeight(): number { - return this.elementRef.nativeElement.clientHeight; + public navigateNext() { + this.navigate(Navigate.Down); } /** - * Get item html element * @hidden */ - public get element() { - return this.elementRef; + public navigatePrev() { + this.navigate(Navigate.Up); } - constructor( - public dropDown: IgxDropDownBase, - protected elementRef: ElementRef - ) { } - - /** - * @hidden - */ - @HostListener('click', ['$event']) - clicked(event) { - if (this.disabled || this.isHeader) { - const focusedItem = this.dropDown.items.find((item) => item.isFocused); - if (this.dropDown.allowItemsFocus && focusedItem) { - focusedItem.elementRef.nativeElement.focus({ preventScroll: true }); - } - return; + protected scrollToHiddenItem(newItem: IgxDropDownItemBase) { + const elementRect = newItem.element.nativeElement.getBoundingClientRect(); + const parentRect = this.scrollContainer.getBoundingClientRect(); + if (parentRect.top > elementRect.top) { + this.scrollContainer.scrollTop -= (parentRect.top - elementRect.top); } - this.dropDown.navigateItem(this.index); - this.dropDown.selectItem(this, event); - } - /** - * @hidden - */ - markItemSelected() { - this.dropDown.setSelectedItem(this.index); - this.dropDown.close(); + if (parentRect.bottom < elementRect.bottom) { + this.scrollContainer.scrollTop += (elementRect.bottom - parentRect.bottom); + } } } diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down.common.ts b/projects/igniteui-angular/src/lib/drop-down/drop-down.common.ts index 9e8cc8ec5ab..c7bfa220def 100644 --- a/projects/igniteui-angular/src/lib/drop-down/drop-down.common.ts +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down.common.ts @@ -1,5 +1,8 @@ import { CancelableEventArgs } from '../core/utils'; -import { IgxDropDownItemBase } from './drop-down.base'; +import { IgxDropDownItemBase } from './drop-down-item.base'; +import { IToggleView } from '../core/navigation/IToggleView'; +import { OnInit, EventEmitter } from '@angular/core'; +import { DropDownActionKey } from './drop-down-navigation.directive'; /** @hidden */ export enum Navigate { @@ -15,3 +18,57 @@ export interface ISelectionEventArgs extends CancelableEventArgs { oldSelection: IgxDropDownItemBase; newSelection: IgxDropDownItemBase; } + +/** + * Interface for an instance of IgxDropDownNavigationDirective + * @export + */ +export interface IDropDownNavigationDirective { + target: any; + handleKeyDown(event: KeyboardEvent): void; + onArrowDownKeyDown(event?: KeyboardEvent): void; + onArrowUpKeyDown(event?: KeyboardEvent): void; + onEndKeyDown(event?: KeyboardEvent): void; + onHomeKeyDown(event?: KeyboardEvent): void; +} + +export const IGX_DROPDOWN_BASE = 'IgxDropDownBaseToken'; + +/** + * @hidden + */ +export interface IDropDownList { + onSelection: EventEmitter; + width: string; + height: string; + id: string; + maxHeight: string; + + collapsed: boolean; + + items: IgxDropDownItemBase[]; + headers: IgxDropDownItemBase[]; + focusedItem: IgxDropDownItemBase; + navigateFirst(): void; + navigateLast(): void; + navigateNext(): void; + navigatePrev(): void; + navigateItem(newIndex: number, direction?: Navigate): void; + onItemActionKey(key: DropDownActionKey, event?: Event): void; +} + +/** + * @hidden + */ +export interface IDropDownBase extends IDropDownList, IToggleView { + onOpening: EventEmitter; + onOpened: EventEmitter; + onClosing: EventEmitter; + onClosed: EventEmitter; + + selectedItem: any; + allowItemsFocus?: boolean; + setSelectedItem(index: number): void; + selectItem(item: IgxDropDownItemBase, event?: Event): void; +} + diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down.component.html b/projects/igniteui-angular/src/lib/drop-down/drop-down.component.html index 1f83777e20d..c3eb07c4d7e 100644 --- a/projects/igniteui-angular/src/lib/drop-down/drop-down.component.html +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down.component.html @@ -1,4 +1,5 @@ -
diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down.component.spec.ts b/projects/igniteui-angular/src/lib/drop-down/drop-down.component.spec.ts index 1f706de1c98..82ae3fbff21 100644 --- a/projects/igniteui-angular/src/lib/drop-down/drop-down.component.spec.ts +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down.component.spec.ts @@ -4,7 +4,8 @@ import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxToggleModule, IgxToggleDirective } from '../directives/toggle/toggle.directive'; import { IgxDropDownItemComponent } from './drop-down-item.component'; -import { IgxDropDownComponent, IgxDropDownModule, ISelectionEventArgs } from './drop-down.component'; +import { IgxDropDownComponent, IgxDropDownModule } from './drop-down.component'; +import { ISelectionEventArgs } from './drop-down.common'; import { IgxTabsComponent, IgxTabsModule } from '../tabs/tabs.component'; import { UIInteractions } from '../test-utils/ui-interactions.spec'; import { CancelableEventArgs } from '../core/utils'; @@ -869,7 +870,8 @@ describe('IgxDropDown ', () => { expect(dropdown.selectItem).toHaveBeenCalledTimes(0); expect(dropdown.collapsed).toEqual(true); expect(dropdown.focusedItem).toEqual(null); - inputElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + const mockEvent = new KeyboardEvent('keydown', { key: 'Enter' }); + inputElement.dispatchEvent(mockEvent); tick(); expect(dropdown.selectItem).toHaveBeenCalledTimes(0); // does not attempt to select item on keydown if DD is closed; expect(dropdown.selectedItem).toEqual(null); @@ -879,10 +881,10 @@ describe('IgxDropDown ', () => { expect(dropdown.collapsed).toEqual(false); expect(dropdown.focusedItem).toEqual(dropdown.items[0]); const dropdownItem = dropdown.items[0]; - inputElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + inputElement.dispatchEvent(mockEvent); tick(); expect(dropdown.selectItem).toHaveBeenCalledTimes(1); - expect(dropdown.selectItem).toHaveBeenCalledWith(dropdownItem, jasmine.any(Object)); + expect(dropdown.selectItem).toHaveBeenCalledWith(dropdownItem, mockEvent); expect(dropdown.selectedItem).toEqual(dropdownItem); expect(dropdown.collapsed).toEqual(true); })); diff --git a/projects/igniteui-angular/src/lib/drop-down/drop-down.component.ts b/projects/igniteui-angular/src/lib/drop-down/drop-down.component.ts index 494b278a40a..fb348c6f879 100644 --- a/projects/igniteui-angular/src/lib/drop-down/drop-down.component.ts +++ b/projects/igniteui-angular/src/lib/drop-down/drop-down.component.ts @@ -5,212 +5,362 @@ import { ContentChildren, ElementRef, forwardRef, - Input, NgModule, QueryList, - Self, - Optional, - HostListener, - Directive + OnInit, + Input, + OnDestroy, + ViewChild, + EventEmitter, + Output, } from '@angular/core'; -import { IgxSelectionAPIService } from '../core/selection'; -import { IgxToggleModule } from '../directives/toggle/toggle.directive'; +import { IgxToggleModule, IgxToggleDirective } from '../directives/toggle/toggle.directive'; import { IgxDropDownItemComponent } from './drop-down-item.component'; -import { IgxComboDropDownComponent } from '../combo/combo-dropdown.component'; -import { IgxDropDownBase, IgxDropDownItemBase } from './drop-down.base'; +import { IgxDropDownBase } from './drop-down.base'; +import { IgxDropDownItemNavigationDirective, DropDownActionKey } from './drop-down-navigation.directive'; +import { IGX_DROPDOWN_BASE, IDropDownBase } from './drop-down.common'; +import { ISelectionEventArgs, Navigate } from './drop-down.common'; +import { CancelableEventArgs, isIE } from '../core/utils'; +import { IgxSelectionAPIService } from '../core/selection'; +import { Subject } from 'rxjs'; +import { IgxDropDownItemBase } from './drop-down-item.base'; +import { OverlaySettings } from '../services'; -@Directive({ - selector: '[igxDropDownItemNavigation]' +/** + * **Ignite UI for Angular DropDown** - + * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/drop-down.html) + * + * The Ignite UI for Angular Drop Down displays a scrollable list of items which may be visually grouped and + * supports selection of a single item. Clicking or tapping an item selects it and closes the Drop Down + * + * Example: + * ```html + * + * + * {{ item.value }} + * + * + * ``` + */ +@Component({ + selector: 'igx-drop-down', + templateUrl: './drop-down.component.html', + providers: [{ provide: IGX_DROPDOWN_BASE, useExisting: IgxDropDownComponent }] }) -export class IgxDropDownItemNavigationDirective { +export class IgxDropDownComponent extends IgxDropDownBase implements IDropDownBase, OnInit, OnDestroy { + @ContentChildren(forwardRef(() => IgxDropDownItemComponent)) + protected children: QueryList; - private _target; + @ViewChild(IgxToggleDirective) + protected toggleDirective: IgxToggleDirective; - constructor(@Self() @Optional() public dropdown: IgxDropDownBase) { } + protected destroy$ = new Subject(); + /** + * Gets/sets whether items take focus. Disabled by default. + * When enabled, drop down items gain tab index and are focused when active - + * this includes activating the selected item when opening the drop down and moving with keyboard navigation. + * + * Note: Keep that focus shift in mind when using the igxDropDownItemNavigation directive + * and ensure it's placed either on each focusable item or a common ancestor to allow it to handle keyboard events. + * + * ```typescript + * // get + * let dropDownAllowsItemFocus = this.dropdown.allowItemsFocus; + * ``` + * + * ```html + * + * + * ``` + */ + @Input() + public allowItemsFocus = false; + + @Input() + get id(): string { + return this._id; + } + set id(value: string) { + this.toggleDirective.id = value; + this.selection.set(value, this.selection.get(this.id)); + this.selection.clear(this.id); + this._id = value; + } /** - * @hidden + * Emitted before the dropdown is opened + * + * ```html + * + * ``` + */ + @Output() + public onOpening = new EventEmitter(); + + /** + * Emitted after the dropdown is opened + * + * ```html + * + * ``` + */ + @Output() + public onOpened = new EventEmitter(); + + /** + * Emitted before the dropdown is closed + * + * ```html + * + * ``` + */ + @Output() + public onClosing = new EventEmitter(); + + /** + * Emitted after the dropdown is closed + * + * ```html + * + * ``` */ - get target() { - return this._target; + @Output() + public onClosed = new EventEmitter(); + + /** + * Get currently selected item + * + * ```typescript + * let currentItem = this.dropdown.selectedItem; + * ``` + */ + public get selectedItem(): any { + const selectedItem = this.selection.first_item(this.id); + if (selectedItem) { + if (selectedItem.isSelected) { + return selectedItem; + } + this.selection.clear(this.id); + } + return null; } /** - * @hidden + * Gets if the dropdown is collapsed + * + * ```typescript + * let isCollapsed = this.dropdown.collapsed; + * ``` */ - @Input('igxDropDownItemNavigation') - set target(target: IgxDropDownBase) { - this._target = target ? target : this.dropdown; + public get collapsed(): boolean { + return this.toggleDirective.collapsed; } - @HostListener('focus') - handleFocus() { - if ((this.target).combo) { - this.target.focusedItem = this.target.getFirstSelectableItem(); - this.target.focusedItem.isFocused = true; - } + + protected get scrollContainer() { + return this.toggleDirective.element; + } + + constructor( + protected elementRef: ElementRef, + protected cdr: ChangeDetectorRef, + protected selection: IgxSelectionAPIService) { + super(elementRef, cdr); } /** - * @hidden + * Opens the dropdown + * + * ```typescript + * this.dropdown.open(); + * ``` */ - @HostListener('keydown', ['$event']) - handleKeyDown(event: KeyboardEvent) { - if (event) { - const key = event.key.toLowerCase(); - if (!this.target.collapsed) { // If dropdown is opened - const navKeys = ['esc', 'escape', 'enter', 'space', 'spacebar', ' ', - 'arrowup', 'up', 'arrowdown', 'down', 'home', 'end']; - if (navKeys.indexOf(key) === -1) { // If key has appropriate function in DD - return; - } - event.preventDefault(); - event.stopPropagation(); - } else { // If dropdown is closed, do nothing - return; - } - switch (key) { - case 'esc': - case 'escape': - // case 'tab': - this.onEscapeKeyDown(event); - break; - case 'enter': - this.onEnterKeyDown(event); - break; - case 'space': - case 'spacebar': - case ' ': - this.onSpaceKeyDown(event); - break; - case 'arrowup': - case 'up': - this.onArrowUpKeyDown(event); - break; - case 'arrowdown': - case 'down': - this.onArrowDownKeyDown(event); - break; - case 'home': - this.onHomeKeyDown(event); - break; - case 'end': - this.onEndKeyDown(event); - break; - default: - return; - } + public open(overlaySettings?: OverlaySettings) { + this.toggleDirective.open(overlaySettings); + } + + /** + * Closes the dropdown + * + * ```typescript + * this.dropdown.close(); + * ``` + */ + public close() { + this.toggleDirective.close(); + } + + /** + * Toggles the dropdown + * + * ```typescript + * this.dropdown.toggle(); + * ``` + */ + public toggle(overlaySettings?: OverlaySettings) { + if (this.toggleDirective.collapsed) { + this.open(overlaySettings); + } else { + this.close(); } } /** - * @hidden + * Select an item by index + * @param index of the item to select */ - onEscapeKeyDown(event) { - this.target.close(); + public setSelectedItem(index: number) { + if (index < 0 || index >= this.items.length) { + return; + } + const newSelection = this.items[index]; + this.selectItem(newSelection); } /** - * @hidden + * Navigates to the item on the specified index + * @param newIndex number */ - onSpaceKeyDown(event) { - // V.S. : IgxDropDownComponent.selectItem needs event to be true in order to close DD as per specification - this.target.selectItem(this.target.focusedItem, this.target instanceof IgxDropDownComponent); + public navigateItem(index: number) { + super.navigateItem(index); + if (this.allowItemsFocus && this.focusedItem) { + this.focusedItem.element.nativeElement.focus(); + this.cdr.markForCheck(); + } } /** * @hidden */ - onEnterKeyDown(event) { - if (!(this.target instanceof IgxDropDownComponent)) { - if (this.target.focusedItem.value === 'ADD ITEM') { - // TODO: refactor: - const targetC = this.target as IgxComboDropDownComponent; - targetC.combo.addItemToCollection(); - } else { - this.target.close(); - } + public onToggleOpening(e: CancelableEventArgs) { + this.onOpening.emit(e); + if (e.cancel) { return; } - this.target.selectItem(this.target.focusedItem, event); + this.scrollToItem(this.selectedItem); } /** * @hidden */ - onArrowDownKeyDown(event) { - this.target.navigateNext(); + public onToggleOpened() { + this._focusedItem = this.selectedItem; + if (this._focusedItem) { + this._focusedItem.isFocused = true; + } else if (this.allowItemsFocus) { + const firstItemIndex = this.getNearestSiblingFocusableItemIndex(-1, Navigate.Down); + if (firstItemIndex !== -1) { + this.navigateItem(firstItemIndex); + } + } + this.onOpened.emit(); } /** * @hidden */ - onArrowUpKeyDown(event) { - this.target.navigatePrev(); + public onToggleClosing(e: CancelableEventArgs) { + this.onClosing.emit(e); } /** * @hidden */ - onEndKeyDown(event) { - this.target.navigateLast(); + public onToggleClosed() { + if (this._focusedItem) { + this._focusedItem.isFocused = false; + } + this.onClosed.emit(); } /** * @hidden */ - onHomeKeyDown(event) { - this.target.navigateFirst(); + public ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.complete(); + this.selection.clear(this.id); } -} -/** - * **Ignite UI for Angular DropDown** - - * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/drop_down.html) - * - * The Ignite UI for Angular Drop Down displays a scrollable list of items which may be visually grouped and - * supports selection of a single item. Clicking or tapping an item selects it and closes the Drop Down - * - * Example: - * ```html - * - * - * {{ item.value }} - * - * - * ``` - */ -@Component({ - selector: 'igx-drop-down', - templateUrl: './drop-down.component.html', - providers: [{ provide: IgxDropDownBase, useExisting: IgxDropDownComponent }] -}) -export class IgxDropDownComponent extends IgxDropDownBase { + protected scrollToItem(item: IgxDropDownItemBase) { + const itemPosition = this.calculateScrollPosition(item); - @ContentChildren(forwardRef(() => IgxDropDownItemComponent)) - protected children: QueryList; + // in IE11 setting sctrollTop is somehow slow and forces dropdown + // to appear on screen before animation start. As a result dropdown + // flickers badly. This is why we set scrollTop just a little later + // allowing animation to start and prevent dropdown flickering + if (isIE()) { + setTimeout(() => { + this.scrollContainer.scrollTop = (itemPosition); + }, 1); + } else { + this.scrollContainer.scrollTop = (itemPosition); + } + } - constructor( - protected elementRef: ElementRef, - protected cdr: ChangeDetectorRef, - protected selection: IgxSelectionAPIService) { - super(elementRef, cdr, selection); + protected calculateScrollPosition(item: IgxDropDownItemBase): number { + if (!item) { + return 0; + } + + const elementRect = item.element.nativeElement.getBoundingClientRect(); + const parentRect = this.scrollContainer.getBoundingClientRect(); + const scrollDelta = parentRect.top - elementRect.top; + let scrollPosition = this.scrollContainer.scrollTop - scrollDelta; + + const dropDownHeight = this.scrollContainer.clientHeight; + scrollPosition -= dropDownHeight / 2; + scrollPosition += item.elementHeight / 2; + + return Math.floor(scrollPosition); + } + + /** + * @hidden + */ + ngOnInit() { + this.toggleDirective.id = this.id; + } + + /** Keydown Handler */ + public onItemActionKey(key: DropDownActionKey, event?: Event) { + super.onItemActionKey(key, event); + this.close(); } - protected changeSelectedItem(newSelection?: IgxDropDownItemComponent): boolean { + /** + * Handles the `onSelection` emit and the drop down toggle when selection changes + * @hidden + * @internal + * @param newSelection + * @param event + */ + public selectItem(newSelection?: IgxDropDownItemBase, event?: Event) { const oldSelection = this.selectedItem; - const selectionChanged = super.changeSelectedItem(newSelection); + if (!newSelection) { + newSelection = this._focusedItem; + } + if (newSelection === null) { + return; + } + if (newSelection.isHeader) { + return; + } + const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false }; + this.onSelection.emit(args); - if (selectionChanged) { + if (!args.cancel) { + this.selection.set(this.id, new Set([newSelection])); if (oldSelection) { oldSelection.isSelected = false; } if (newSelection) { newSelection.isSelected = true; } + if (event) { + this.toggleDirective.close(); + } } - - return selectionChanged; } } @@ -224,5 +374,3 @@ export class IgxDropDownComponent extends IgxDropDownBase { providers: [IgxSelectionAPIService] }) export class IgxDropDownModule { } - -export { ISelectionEventArgs } from './drop-down.common'; diff --git a/projects/igniteui-angular/src/lib/drop-down/index.ts b/projects/igniteui-angular/src/lib/drop-down/index.ts new file mode 100644 index 00000000000..f1fb80184fa --- /dev/null +++ b/projects/igniteui-angular/src/lib/drop-down/index.ts @@ -0,0 +1,6 @@ +export * from './drop-down.component'; +export * from './drop-down-item.component'; +export { ISelectionEventArgs, IDropDownNavigationDirective, } from './drop-down.common'; +export * from './drop-down-navigation.directive'; +export * from './drop-down.base'; +export * from './drop-down-item.base'; diff --git a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-row.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-row.component.ts index e58ed1495ca..44cc57b6902 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-row.component.ts @@ -15,7 +15,7 @@ import { import { Subject } from 'rxjs'; import { DataType } from '../../data-operations/data-util'; import { IgxColumnComponent } from '../column.component'; -import { IgxDropDownComponent, ISelectionEventArgs } from '../../drop-down/drop-down.component'; +import { IgxDropDownComponent, ISelectionEventArgs } from '../../drop-down/index'; import { IFilteringOperation } from '../../data-operations/filtering-condition'; import { FilteringLogic, IFilteringExpression } from '../../data-operations/filtering-expression.interface'; import { HorizontalAlignment, VerticalAlignment, OverlaySettings } from '../../services/overlay/utilities'; diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index 298f2abcc51..7198185eaf8 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts @@ -20,6 +20,7 @@ import { AnimationBuilder, AnimationReferenceMetadata, AnimationMetadataType, An import { fromEvent, Subject } from 'rxjs'; import { take, filter, takeUntil } from 'rxjs/operators'; import { IAnimationParams } from '../../animations/main'; +import { ElasticPositionStrategy } from './position'; /** * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/overlay_main.html) @@ -166,7 +167,10 @@ export class IgxOverlayService implements OnDestroy { this.updateSize(info); this._overlayInfos.push(info); - info.originalElementStyle = info.elementRef.nativeElement.style; + const elementStyle = info.elementRef.nativeElement.style; + if (settings.positionStrategy instanceof ElasticPositionStrategy) { + info.originalElementStyleSize = { width: elementStyle.width, height: elementStyle.height }; + } settings.positionStrategy.position( info.elementRef.nativeElement.parentElement, { width: info.initialSize.width, height: info.initialSize.height }, @@ -260,7 +264,10 @@ export class IgxOverlayService implements OnDestroy { overlayInfo.settings.positionStrategy.position( overlayInfo.elementRef.nativeElement.parentElement, - { width: overlayInfo.initialSize.width, height: overlayInfo.initialSize.height }, + { + width: overlayInfo.elementRef.nativeElement.parentElement.clientWidth, + height: overlayInfo.elementRef.nativeElement.parentElement.clientHeight + }, this._document, false, overlayInfo.settings.positionStrategy.settings.minSize); @@ -403,7 +410,13 @@ export class IgxOverlayService implements OnDestroy { this._overlayElement.parentElement.removeChild(this._overlayElement); this._overlayElement = null; } - info.elementRef.nativeElement.style = info.originalElementStyle; + + // restore the element's original width and height if any + if (info.originalElementStyleSize) { + info.elementRef.nativeElement.style.height = info.originalElementStyleSize.height; + info.elementRef.nativeElement.style.width = info.originalElementStyleSize.width; + } + this.onClosed.emit({ id: info.id, componentRef: info.componentRef }); } diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/elastic-position-strategy.ts b/projects/igniteui-angular/src/lib/services/overlay/position/elastic-position-strategy.ts index d598b3aa881..2ceceb510dd 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/position/elastic-position-strategy.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/position/elastic-position-strategy.ts @@ -4,50 +4,51 @@ import { Size, HorizontalAlignment, VerticalAlignment, PositionSettings } from ' export class ElasticPositionStrategy extends BaseFitPositionStrategy implements IPositionStrategy { fitHorizontal(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size) { + let extend = 0; switch (settings.horizontalDirection) { case HorizontalAlignment.Left: { - let extend = outerRect.left - innerRect.left; + extend = outerRect.left - innerRect.left; if (extend > innerRect.width - minSize.width) { extend = innerRect.width - minSize.width; } const translateX = `translateX(${innerRect.left + extend}px)`; element.style.transform = element.style.transform.replace(/translateX\([.-\d]+px\)/g, translateX); - (element.firstChild).style.width = `${innerRect.width - extend}px`; break; } case HorizontalAlignment.Right: { - let extend = innerRect.right - outerRect.right; + extend = innerRect.right - outerRect.right; if (extend > innerRect.width - minSize.width) { extend = innerRect.width - minSize.width; } - - (element.firstChild).style.width = `${innerRect.width - extend}px`; break; } } + element.style.width = `${innerRect.width - extend}px`; + (element.firstChild).style.width = `${innerRect.width - extend}px`; } fitVertical(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size) { + let extend = 0; switch (settings.verticalDirection) { case VerticalAlignment.Top: { - let extend = outerRect.top - innerRect.top; + extend = outerRect.top - innerRect.top; if (extend > innerRect.height - minSize.height) { extend = innerRect.height - minSize.height; } const translateY = `translateY(${innerRect.top + extend}px)`; element.style.transform = element.style.transform.replace(/translateY\([.-\d]+px\)/g, translateY); - (element.firstChild).style.height = `${innerRect.width - extend}px`; break; } case VerticalAlignment.Bottom: { - let extend = innerRect.bottom - outerRect.bottom; + extend = innerRect.bottom - outerRect.bottom; if (extend > innerRect.height - minSize.height) { extend = innerRect.height - minSize.height; } - - (element.firstChild).style.height = `${innerRect.height - extend}px`; break; } } + + element.style.height = `${innerRect.height - extend}px`; + (element.firstChild).style.height = `${innerRect.height - extend}px`; } } diff --git a/projects/igniteui-angular/src/lib/services/overlay/utilities.ts b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts index 1cdab04f92b..d37e15c156b 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/utilities.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts @@ -117,5 +117,5 @@ export interface OverlayInfo { closeAnimationPlayer?: AnimationPlayer; openAnimationInnerPlayer?: any; closeAnimationInnerPlayer?: any; - originalElementStyle?: string; + originalElementStyleSize?: Size; } diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 25e716ae5ec..c1cc2ff7a34 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -57,11 +57,10 @@ export * from './lib/card/card.component'; export * from './lib/carousel/carousel.component'; export * from './lib/checkbox/checkbox.component'; export * from './lib/chips/index'; -export * from './lib/combo/combo.component'; +export * from './lib/combo/index'; export * from './lib/date-picker/date-picker.component'; export * from './lib/dialog/dialog.component'; -export * from './lib/drop-down/drop-down.component'; -export * from './lib/drop-down/drop-down-item.component'; +export * from './lib/drop-down/index'; export * from './lib/grids/grid/index'; export * from './lib/grids/tree-grid/index'; export * from './lib/icon/index'; diff --git a/src/app/combo/combo.sample.css b/src/app/combo/combo.sample.css index a03634ef44c..0482f21b17d 100644 --- a/src/app/combo/combo.sample.css +++ b/src/app/combo/combo.sample.css @@ -39,7 +39,7 @@ flex-wrap: wrap; } -.dropdownToggleButton { +.igx-combo__clear-button { position: relative; } diff --git a/src/app/combo/combo.sample.html b/src/app/combo/combo.sample.html index 1302cf063b0..164042634fc 100644 --- a/src/app/combo/combo.sample.html +++ b/src/app/combo/combo.sample.html @@ -18,6 +18,7 @@
{ + this.igxCombo.onOpening.subscribe(() => { console.log('Opening log!'); }); - this.igxCombo.dropdown.onOpened.subscribe(() => { + this.igxCombo.onOpened.subscribe(() => { console.log('Opened log!'); }); - this.igxCombo.dropdown.onOpened.pipe(take(1)).subscribe(() => { + this.igxCombo.onOpened.pipe(take(1)).subscribe(() => { console.log('Attaching'); if (this.igxCombo.searchInput) { this.igxCombo.searchInput.nativeElement.onchange = (e) => { @@ -112,11 +110,11 @@ export class ComboSampleComponent implements OnInit { } }); - this.igxCombo.dropdown.onClosing.subscribe(() => { + this.igxCombo.onClosing.subscribe(() => { console.log('Closing log!'); }); - this.igxCombo.dropdown.onClosed.subscribe(() => { + this.igxCombo.onClosed.subscribe(() => { console.log('Closed log!'); }); @@ -134,4 +132,8 @@ export class ComboSampleComponent implements OnInit { setDensity(density: DisplayDensity) { this.igxCombo.displayDensity = density; } + + handleSelectionChange(event: IComboSelectionChangeEventArgs) { + console.log(event); + } } diff --git a/src/app/overlay/overlay.sample.html b/src/app/overlay/overlay.sample.html index 57b558c1bcf..a2391bf343d 100644 --- a/src/app/overlay/overlay.sample.html +++ b/src/app/overlay/overlay.sample.html @@ -61,7 +61,7 @@
- +
{{ item }} diff --git a/src/app/overlay/overlay.sample.ts b/src/app/overlay/overlay.sample.ts index 3e95037c2cf..66e89e55b0f 100644 --- a/src/app/overlay/overlay.sample.ts +++ b/src/app/overlay/overlay.sample.ts @@ -27,13 +27,13 @@ export class OverlaySampleComponent { modal: true, closeOnOutsideClick: true }; - items = [ - 'Item 1', - 'Item 2', - 'Item 3', - 'Item 4', - 'Item 5' - ]; + constructor() { + for (let item = 0; item < 100; item++) { + this.items.push(`Item ${item}`); + } + } + + items = []; buttonLeft = 90; buttonTop = 35; @@ -159,7 +159,7 @@ export class OverlaySampleComponent { case 'Elastic': this._overlaySettings = { positionStrategy: new ElasticPositionStrategy({ - minSize: { width: 50, height: 50 } + minSize: { width: 150, height: 150 } }), scrollStrategy: new NoOpScrollStrategy(), modal: true, @@ -209,7 +209,7 @@ export class OverlaySampleComponent { 'Connected': new ConnectedPositioningStrategy(), 'Global': new GlobalPositionStrategy(), 'Elastic': new ElasticPositionStrategy({ - minSize: { width: 50, height: 50 } + minSize: { width: 150, height: 150 } }), }, 'VerticalDirection': {