diff --git a/projects/igniteui-angular/grids/core/src/filtering/base/grid-filtering-row.component.ts b/projects/igniteui-angular/grids/core/src/filtering/base/grid-filtering-row.component.ts index 13e234e0f0a..b1699778d66 100644 --- a/projects/igniteui-angular/grids/core/src/filtering/base/grid-filtering-row.component.ts +++ b/projects/igniteui-angular/grids/core/src/filtering/base/grid-filtering-row.component.ts @@ -12,11 +12,14 @@ import { ChangeDetectionStrategy, ViewRef, HostListener, - OnDestroy + OnDestroy, + InjectionToken, + inject, + OnInit } from '@angular/core'; import { IgxFilteringService } from '../grid-filtering.service'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { debounceTime, takeUntil } from 'rxjs/operators'; import { ExpressionUI } from '../excel-style/common'; import { NgTemplateOutlet, NgClass } from '@angular/common'; import { IgxDropDownComponent, IgxDropDownItemComponent, IgxDropDownItemNavigationDirective, ISelectionEventArgs } from 'igniteui-angular/drop-down'; @@ -28,6 +31,14 @@ import { AbsoluteScrollStrategy, ColumnType, ConnectedPositioningStrategy, DataU import { IgxTimePickerComponent } from 'igniteui-angular/time-picker'; import { IgxButtonDirective, IgxDateTimeEditorDirective, IgxIconButtonDirective, IgxRippleDirective } from 'igniteui-angular/directives'; +/** + * Injection token for setting the debounce time used in filtering row inputs. + * @hidden + */ +export const INPUT_DEBOUNCE_TIME = /*@__PURE__*/new InjectionToken('INPUT_DEBOUNCE_TIME', { + factory: () => 350 +}); + /** * @hidden */ @@ -58,7 +69,7 @@ import { IgxButtonDirective, IgxDateTimeEditorDirective, IgxIconButtonDirective, IgxIconButtonDirective ] }) -export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy { +export class IgxGridFilteringRowComponent implements OnInit, AfterViewInit, OnDestroy { @Input() public get column(): ColumnType { return this._column; @@ -198,7 +209,10 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy { /** switch to icon buttons when width is below 432px */ private readonly NARROW_WIDTH_THRESHOLD = 432; + private inputSubject: Subject = new Subject(); + private $destroyer = new Subject(); + private readonly DEBOUNCE_TIME = inject(INPUT_DEBOUNCE_TIME); constructor( public filteringService: IgxFilteringService, @@ -207,12 +221,22 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy { protected platform: PlatformUtil, ) { } + public ngOnInit(): void { + this.inputSubject.pipe( + debounceTime(this.DEBOUNCE_TIME), + takeUntil(this.$destroyer) + ).subscribe(event => { + this.handleInputChange(event); + this.cdr.markForCheck(); // ChangeDetectionStrategy.OnPush is not picking the latest changes of the updated value because of the async pipe + debounce. + }); + } + @HostListener('keydown', ['$event']) public onKeydownHandler(evt: KeyboardEvent) { if (this.platform.isFilteringKeyCombo(evt)) { - evt.preventDefault(); - evt.stopPropagation(); - this.close(); + evt.preventDefault(); + evt.stopPropagation(); + this.close(); } } @@ -227,10 +251,10 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy { } this.filteringService.grid.localeChange - .pipe(takeUntil(this.$destroyer)) - .subscribe(() => { - this.cdr.markForCheck(); - }); + .pipe(takeUntil(this.$destroyer)) + .subscribe(() => { + this.cdr.markForCheck(); + }); requestAnimationFrame(() => this.focusEditElement()); } @@ -337,6 +361,10 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy { * Event handler for input on the input. */ public onInput(eventArgs) { + this.inputSubject.next(eventArgs); + } + + private handleInputChange(eventArgs) { if (!eventArgs) { return; } diff --git a/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts index 69884712f7d..dd8346c89c1 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts @@ -3,6 +3,7 @@ import { fakeAsync, TestBed, tick, flush, ComponentFixture, waitForAsync } from import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxInputDirective, IgxInputGroupComponent } from 'igniteui-angular/input-group'; +import { INPUT_DEBOUNCE_TIME } from 'igniteui-angular/grids/core'; import { IgxGridComponent } from './grid.component'; import { UIInteractions, wait } from '../../../test-utils/ui-interactions.spec'; import { IgxGridFilteringCellComponent } from 'igniteui-angular/grids/core'; @@ -68,6 +69,9 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { const today = SampleTestData.today; beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + providers: [{ provide: INPUT_DEBOUNCE_TIME, useValue: 0 }] + }); fix = TestBed.createComponent(IgxGridFilteringComponent); fix.detectChanges(); grid = fix.componentInstance.grid; @@ -135,6 +139,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // ends with GridFunctions.openFilterDDAndSelectCondition(fix, 3); GridFunctions.typeValueInFilterRowInput('script', fix, input); + tick(); + fix.detectChanges(); expect(grid.rowList.length).toEqual(2); verifyFilterRowUI(input, close, reset, false); @@ -142,6 +148,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // does not contain GridFunctions.openFilterDDAndSelectCondition(fix, 1); GridFunctions.typeValueInFilterRowInput('script', fix, input); + tick(); + fix.detectChanges(); verifyFilterRowUI(input, close, reset, false); expect(grid.rowList.length).toEqual(6); @@ -205,6 +213,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // does not equal GridFunctions.openFilterDDAndSelectCondition(fix, 1); GridFunctions.typeValueInFilterRowInput(100, fix, input); + tick(); + fix.detectChanges(); expect(grid.rowList.length).toEqual(7); verifyFilterRowUI(input, close, reset, false); @@ -217,6 +227,9 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // greater than or equal to GridFunctions.openFilterDDAndSelectCondition(fix, 4); GridFunctions.typeValueInFilterRowInput(254, fix, input); + tick(); + fix.detectChanges(); + expect(grid.rowList.length).toEqual(3); verifyFilterRowUI(input, close, reset, false); @@ -455,8 +468,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { GridFunctions.clickFilterCellChipUI(fix, 'ReleaseDateTime'); let filterUIRow = fix.debugElement.query(By.css(FILTER_UI_ROW)); - let inputDirectiveInstance = filterUIRow.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + let inputDirectiveInstance = filterUIRow.query(By.directive(IgxDateTimeEditorDirective)) + .injector.get(IgxDateTimeEditorDirective); expect(inputDirectiveInstance.inputFormat).toMatch('dd-MM-yyyy'); expect(inputDirectiveInstance.displayFormat).toMatch('yyyy-dd-MM'); @@ -489,8 +502,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { fix.detectChanges(); let filterUIRow = fix.debugElement.query(By.css(FILTER_UI_ROW)); - let dateTimeEditor = filterUIRow.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + let dateTimeEditor = filterUIRow.query(By.directive(IgxDateTimeEditorDirective)) + .injector.get(IgxDateTimeEditorDirective); expect(dateTimeEditor.inputFormat).toMatch('yyyy--dd--MM'); expect(dateTimeEditor.displayFormat).toMatch('yyyy--dd--MM'); @@ -500,7 +513,7 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { filterUIRow = fix.debugElement.query(By.css(FILTER_UI_ROW)); dateTimeEditor = filterUIRow.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + .injector.get(IgxDateTimeEditorDirective); // since 'shortTime' is numeric, input format will include its numeric parts expect(dateTimeEditor.inputFormat.normalize('NFKC')).toMatch('hh:mm tt'); expect(dateTimeEditor.displayFormat).toMatch('shortTime'); @@ -517,6 +530,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { const close = filterUIRow.queryAll(By.css('button'))[1]; GridFunctions.typeValueInFilterRowInput('a', fix, input); + tick(); + fix.detectChanges(); expect(grid.rowList.length).toEqual(1); expect(grid.getCellByColumn(0, 'AnotherField').value).toMatch('custom'); @@ -607,6 +622,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { GridFunctions.clickFilterCellChip(fix, 'ProductName'); GridFunctions.typeValueInFilterRowInput(filterValue, fix); + tick(); + fix.detectChanges(); const filterUIRow = fix.debugElement.query(By.css(FILTER_UI_ROW)); const filterChip = filterUIRow.query(By.directive(IgxChipComponent)); @@ -626,6 +643,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { tick(16); // onConditionsChanged rAF GridFunctions.typeValueInFilterRowInput(filterValue, fix); + tick(); + fix.detectChanges(); const filterUIRow = fix.debugElement.query(By.css(FILTER_UI_ROW)); const filterChip = filterUIRow.query(By.directive(IgxChipComponent)); @@ -633,8 +652,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { expect(filterChip.componentInstance.selected).toBeTruthy(); grid.nativeElement.focus(); - fix.detectChanges(); tick(100); + fix.detectChanges(); expect(filterChip.componentInstance.selected).toBeFalsy(); })); @@ -981,21 +1000,29 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // Set input and confirm GridFunctions.typeValueInFilterRowInput('-1', fix); + tick(); + fix.detectChanges(); expect(input.componentInstance.value).toEqual('-1'); expect(grid.rowList.length).toEqual(1); GridFunctions.typeValueInFilterRowInput('0', fix); + tick(); + fix.detectChanges(); expect(input.componentInstance.value).toEqual('0'); expect(grid.rowList.length).toEqual(0); GridFunctions.typeValueInFilterRowInput('-0.5', fix); + tick(); + fix.detectChanges(); expect(input.componentInstance.value).toEqual('-0.5'); expect(grid.rowList.length).toEqual(1); GridFunctions.typeValueInFilterRowInput('', fix); + tick(); + fix.detectChanges(); expect(input.componentInstance.value).toEqual(null); expect(grid.rowList.length).toEqual(3); @@ -1020,6 +1047,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // Set input and confirm GridFunctions.typeValueInFilterRowInput('a', fix, input); + tick(); + fix.detectChanges(); // Check a chip is created after input and is marked as selected. const filterChip = filteringRow.query(By.directive(IgxChipComponent)); @@ -1653,6 +1682,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // Type 'ang' in the filter row input. GridFunctions.typeValueInFilterRowInput('ang', fix); + tick(); + fix.detectChanges(); // Verify chip is selected (in edit mode). const filteringRow = fix.debugElement.query(By.directive(IgxGridFilteringRowComponent)); @@ -1675,6 +1706,8 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // Type 'ang' in the filter row input. GridFunctions.typeValueInFilterRowInput('ang', fix); + tick(); + fix.detectChanges(); // Verify chip is selected (in edit mode). const filteringRow = fix.debugElement.query(By.directive(IgxGridFilteringRowComponent)); @@ -1720,7 +1753,7 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { filterUIRow = grid.theadRow.filterRow; expect(filterUIRow).toBeUndefined(); - })); + })); it('Should navigate to first cell of grid when pressing \'Tab\' on the last filterCell chip.', fakeAsync(() => { pending('Should be fixed with headers navigation'); @@ -2331,7 +2364,7 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { fix.detectChanges(); const prefix = GridFunctions.getFilterRowPrefix(fix).nativeElement; - UIInteractions.triggerKeyDownEvtUponElem('ArrowRight' , prefix); + UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', prefix); fix.detectChanges(); expect(console.error).not.toHaveBeenCalled(); @@ -2343,6 +2376,9 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { let grid: IgxGridComponent; beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + providers: [{ provide: INPUT_DEBOUNCE_TIME, useValue: 0 }] + }); fix = TestBed.createComponent(IgxGridFilteringComponent); fix.detectChanges(); grid = fix.componentInstance.grid; @@ -2417,10 +2453,11 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { // Add first chip. GridFunctions.typeValueInFilterRowInput('a', fix); - tick(100); + tick(); + fix.detectChanges(); grid.getColumnByName('ProductName').hidden = true; - tick(100); + tick(); fix.detectChanges(); // Check that the filterRow is closed @@ -4355,7 +4392,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { expect(displayContainerRect.height > listHeight + itemHeight && displayContainerRect.height < listHeight + (itemHeight * 2)).toBe(true, 'incorrect search display container height'); // Verify rendered list items count. const listItems = displayContainer.querySelectorAll('igx-list-item'); - expect(listItems.length).toBe(Math.ceil(listHeight / itemHeight ) + 1, 'incorrect rendered list items count'); + expect(listItems.length).toBe(Math.ceil(listHeight / itemHeight) + 1, 'incorrect rendered list items count'); })); it('should correctly display all items in search list after filtering it', (async () => { @@ -5185,7 +5222,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { tick(200); const dateTimeEditor = fix.debugElement.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + .injector.get(IgxDateTimeEditorDirective); expect(dateTimeEditor.inputFormat).toMatch(column.editorOptions.dateTimeFormat); expect(dateTimeEditor.displayFormat).toMatch(column.pipeArgs.format); })); @@ -5211,7 +5248,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { tick(200); const dateTimeEditorDirective = fix.debugElement.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + .injector.get(IgxDateTimeEditorDirective); expect(dateTimeEditorDirective.inputFormat.normalize('NFKC')).toMatch('dd-MM-yyyy'); expect(dateTimeEditorDirective.displayFormat.normalize('NFKC')).toMatch('dd-MM-yyyy'); })); @@ -5234,7 +5271,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { tick(200); const dateTimeEditorDirective = fix.debugElement.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + .injector.get(IgxDateTimeEditorDirective); expect(dateTimeEditorDirective.locale).toMatch(grid.locale); })); @@ -5254,7 +5291,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { tick(200); const dateTimeEditorDirective = fix.debugElement.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + .injector.get(IgxDateTimeEditorDirective); expect(dateTimeEditorDirective.inputFormat).toMatch(column.editorOptions.dateTimeFormat); expect(dateTimeEditorDirective.displayFormat).toMatch(column.pipeArgs.format); })); @@ -5275,7 +5312,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { tick(200); const dateTimeEditorDirective = fix.debugElement.query(By.directive(IgxDateTimeEditorDirective)) - .injector.get(IgxDateTimeEditorDirective); + .injector.get(IgxDateTimeEditorDirective); expect(dateTimeEditorDirective.inputFormat).toMatch(column.pipeArgs.format); expect(dateTimeEditorDirective.displayFormat).toMatch(column.pipeArgs.format); })); @@ -5594,7 +5631,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { const checkboxes: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix)); expect(checkboxes[0].indeterminate).toBeTrue(); expect(checkboxes[1].checked).toBeFalse(); - const listItemsCheckboxes = checkboxes.slice(2, checkboxes.length-1); + const listItemsCheckboxes = checkboxes.slice(2, checkboxes.length - 1); for (const checkboxItem of listItemsCheckboxes) { ControlsFunction.verifyCheckboxState(checkboxItem.parentElement); } @@ -6562,7 +6599,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { // Scroll the search list to the bottom. let scrollbar = GridFunctions.getExcelStyleSearchComponentScrollbar(fix); expect(scrollbar.scrollTop).toBe(0); - let listItems = GridFunctions.getExcelStyleSearchComponentListItems(fix); + const listItems = GridFunctions.getExcelStyleSearchComponentListItems(fix); expect(listItems[0].innerText).toBe('Select All'); scrollbar.scrollTop = 3000; @@ -7093,7 +7130,8 @@ describe('IgxGrid - Custom Filtering Strategy #grid', () => { imports: [ NoopAnimationsModule, CustomFilteringStrategyComponent - ] + ], + providers: [{ provide: INPUT_DEBOUNCE_TIME, useValue: 0 }] }).compileComponents(); })); @@ -7114,13 +7152,12 @@ describe('IgxGrid - Custom Filtering Strategy #grid', () => { it('Should be able to override getFieldValue method', fakeAsync(() => { GridFunctions.clickFilterCellChipUI(fix, 'Name'); // Name column contains nested object as a value - tick(150); fix.detectChanges(); GridFunctions.typeValueInFilterRowInput('ca', fix); - tick(DEBOUNCE_TIME); + tick(); + fix.detectChanges(); GridFunctions.submitFilterRowInput(fix); - tick(DEBOUNCE_TIME); fix.detectChanges(); expect(grid.filteredData).toEqual([]); @@ -7132,13 +7169,12 @@ describe('IgxGrid - Custom Filtering Strategy #grid', () => { grid.filterStrategy = fix.componentInstance.strategy; fix.detectChanges(); GridFunctions.clickFilterCellChipUI(fix, 'Name'); // Name column contains nested object as a value - tick(150); fix.detectChanges(); GridFunctions.typeValueInFilterRowInput('ca', fix); - tick(DEBOUNCE_TIME); + tick(); + fix.detectChanges(); GridFunctions.submitFilterRowInput(fix); - tick(DEBOUNCE_TIME); fix.detectChanges(); expect(grid.filteredData).toEqual( diff --git a/projects/igniteui-angular/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/test-utils/grid-functions.spec.ts index 16b48320d24..a46159be685 100644 --- a/projects/igniteui-angular/test-utils/grid-functions.spec.ts +++ b/projects/igniteui-angular/test-utils/grid-functions.spec.ts @@ -658,6 +658,8 @@ export class GridFunctions { const filterUIRow = fix.debugElement.query(By.css(FILTER_UI_ROW)); const input = filterUIRow.query(By.directive(IgxInputDirective)); UIInteractions.clickAndSendInputElementValue(input.nativeElement, value, fix); + tick(); // Needed because of the debounce time in filtering row input + fix.detectChanges(); // Enter key to submit UIInteractions.triggerEventHandlerKeyDown('Enter', input);