From 518bd16c0747f3b392737e6ba78f91ce3d27e5af Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Thu, 23 Oct 2025 11:41:56 +0300 Subject: [PATCH 1/3] feat(filtering): Add debounce time when entering filter value --- .../base/grid-filtering-row.component.ts | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/filtering/base/grid-filtering-row.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/base/grid-filtering-row.component.ts index ef7ab4f3b0a..4d088192b9f 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/base/grid-filtering-row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/base/grid-filtering-row.component.ts @@ -12,7 +12,10 @@ import { ChangeDetectionStrategy, ViewRef, HostListener, - OnDestroy + OnDestroy, + InjectionToken, + inject, + OnInit } from '@angular/core'; import { GridColumnDataType, DataUtil } from '../../../data-operations/data-util'; import { IgxDropDownComponent } from '../../../drop-down/drop-down.component'; @@ -28,7 +31,7 @@ import { IgxDatePickerComponent } from '../../../date-picker/date-picker.compone import { IgxTimePickerComponent } from '../../../time-picker/time-picker.component'; import { isEqual, PlatformUtil } from '../../../core/utils'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { debounceTime, takeUntil } from 'rxjs/operators'; import { ExpressionUI } from '../excel-style/common'; import { ColumnType } from '../../common/grid.interface'; import { IgxRippleDirective } from '../../../directives/ripple/ripple.directive'; @@ -47,6 +50,14 @@ import { NgTemplateOutlet, NgClass } from '@angular/common'; import { IgxIconButtonDirective } from '../../../directives/button/icon-button.directive'; import { Size } from '../../common/enums'; +/** + * 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 */ @@ -56,7 +67,7 @@ import { Size } from '../../common/enums'; templateUrl: './grid-filtering-row.component.html', imports: [IgxDropDownComponent, IgxDropDownItemComponent, IgxChipsAreaComponent, IgxChipComponent, IgxIconComponent, IgxInputGroupComponent, IgxPrefixDirective, IgxDropDownItemNavigationDirective, IgxInputDirective, IgxSuffixDirective, IgxDatePickerComponent, IgxPickerToggleComponent, IgxPickerClearComponent, IgxTimePickerComponent, IgxDateTimeEditorDirective, NgTemplateOutlet, IgxButtonDirective, NgClass, IgxRippleDirective, IgxIconButtonDirective] }) -export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy { +export class IgxGridFilteringRowComponent implements OnInit, AfterViewInit, OnDestroy { @Input() public get column(): ColumnType { return this._column; @@ -196,7 +207,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, @@ -205,12 +219,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(); } } @@ -225,10 +249,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()); } @@ -335,6 +359,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; } From f5e74414e2b24e3acbfe626ff10baf950fc92a25 Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Thu, 23 Oct 2025 11:45:05 +0300 Subject: [PATCH 2/3] test(filtering): Fix tests because of introduced debounce --- .../lib/grids/grid/grid-filtering-ui.spec.ts | 87 +++++++++++++------ .../src/lib/test-utils/grid-functions.spec.ts | 2 + 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts index 265eb1786ca..d9f44924f4c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts @@ -16,7 +16,7 @@ import { import { IgxDatePickerComponent } from '../../date-picker/date-picker.component'; import { IgxGridFilteringCellComponent } from '../filtering/base/grid-filtering-cell.component'; import { IgxGridHeaderComponent } from '../headers/grid-header.component'; -import { IgxGridFilteringRowComponent } from '../filtering/base/grid-filtering-row.component'; +import { IgxGridFilteringRowComponent, INPUT_DEBOUNCE_TIME } from '../filtering/base/grid-filtering-row.component'; import { GridFunctions, GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; import { IgxBadgeComponent } from '../../badge/badge.component'; import { IgxIconComponent } from '../../icon/icon.component'; @@ -51,7 +51,7 @@ import { GridSelectionMode, FilterMode, Size } from '../common/enums'; import { ControlsFunction } from '../../test-utils/controls-functions.spec'; import { FilteringStrategy, FormattedValuesFilteringStrategy } from '../../data-operations/filtering-strategy'; import { IgxInputGroupComponent } from '../../input-group/public_api'; -import { formatDate, getComponentSize } from '../../core/utils'; +import { getComponentSize } from '../../core/utils'; import { IgxCalendarComponent } from '../../calendar/calendar.component'; import { GridResourceStringsEN } from '../../core/i18n/grid-resources'; import { setElementSize } from '../../test-utils/helper-utils.spec'; @@ -87,6 +87,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; @@ -154,6 +157,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); @@ -161,6 +166,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); @@ -224,6 +231,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); @@ -236,6 +245,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); @@ -474,8 +486,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'); @@ -508,8 +520,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'); @@ -519,7 +531,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'); @@ -536,6 +548,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'); @@ -626,6 +640,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)); @@ -645,6 +661,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)); @@ -652,8 +670,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(); })); @@ -1000,21 +1018,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); @@ -1039,6 +1065,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)); @@ -1672,6 +1700,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)); @@ -1694,6 +1724,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)); @@ -1739,7 +1771,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'); @@ -2350,7 +2382,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(); @@ -2362,6 +2394,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; @@ -2436,10 +2471,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 @@ -4374,7 +4410,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 () => { @@ -5204,7 +5240,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); })); @@ -5230,7 +5266,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'); })); @@ -5253,7 +5289,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); })); @@ -5273,7 +5309,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); })); @@ -5294,7 +5330,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); })); @@ -5613,7 +5649,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); } @@ -7115,7 +7151,8 @@ describe('IgxGrid - Custom Filtering Strategy #grid', () => { imports: [ NoopAnimationsModule, CustomFilteringStrategyComponent - ] + ], + providers: [{ provide: INPUT_DEBOUNCE_TIME, useValue: 0 }] }).compileComponents(); })); @@ -7136,13 +7173,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([]); @@ -7154,13 +7190,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/src/lib/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts index aec6b388b6c..b05412c526a 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts @@ -669,6 +669,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); From 6db4c637ec14498eb852de8ec86fecbe38d04386 Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Fri, 21 Nov 2025 16:26:42 +0200 Subject: [PATCH 3/3] chore(*): fix 1 more import left from the merge --- .../igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts | 1 + 1 file changed, 1 insertion(+) 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 ccb9ea4096e..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';