Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
acba841
Initial plan
Copilot Mar 30, 2026
38d8fb3
test(grid): add reproduction test for ESF search not matching numbers…
Copilot Mar 30, 2026
436df63
fix(grid): ESF search matches numeric values without locale formatting
Copilot Mar 30, 2026
c194f75
fix(grid): ESF search matches numeric values without locale formatting
Copilot Mar 30, 2026
f64877c
Merge branch 'master' into copilot/fix-numeric-filtering-in-esf
kdinev Mar 30, 2026
feae3bd
chore(*): fixing package-lock
Mar 30, 2026
1f31b1f
Merge branch 'master' into copilot/fix-numeric-filtering-in-esf
kdinev Mar 31, 2026
21ad1b0
chore(*): updating demos for tests
Mar 31, 2026
963abd4
chore(*): extending cell-merging demo to test further
Mar 31, 2026
004391d
Merge branch 'master' into copilot/fix-numeric-filtering-in-esf
kdinev Mar 31, 2026
3b29d85
chore(*): fixing template quotes on h-grid-updating demo
Mar 31, 2026
9a1079a
Merge branch 'copilot/fix-numeric-filtering-in-esf' of https://github…
Mar 31, 2026
8bc860d
Merge branch 'master' into copilot/fix-numeric-filtering-in-esf
kdinev Mar 31, 2026
a98950e
chore(h-grid-updating): fixing demo template indentation
Mar 31, 2026
d01aa20
Merge branch 'master' into copilot/fix-numeric-filtering-in-esf
kdinev Mar 31, 2026
e5e2f3c
Merge branch 'master' into copilot/fix-numeric-filtering-in-esf
kdinev Apr 1, 2026
dd86e81
Merge branch 'master' into copilot/fix-numeric-filtering-in-esf
kdinev Apr 1, 2026
cfe2463
chore(excel-style-search): Add a comment to test the behavior when hi…
mddragnev Apr 1, 2026
b2ce2c9
Merge branch 'copilot/fix-numeric-filtering-in-esf' of https://github…
mddragnev Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -482,17 +482,26 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy {
const matchedData = cloneHierarchicalArray(this.esf.listData, 'children');
this.displayedListData = this.hierarchicalSelectMatches(matchedData, searchVal);
this.cdr.detectChanges();
/**
* There are two calls of `matchesNumericValue` in this method: one when we generate the displayedListData in hierarchicalSelectMatches method
* and another one when going through the tree nodes. We can avoid the second call by storing the items in a set.
* However, if the datasource is small there is no significant difference in performance but we would be adding extra memory overhead.
* We should test this when https://github.com/IgniteUI/igniteui-angular/issues/17144 issue is fixed with 100k or 1m records
*/
this.tree.nodes.forEach(n => {
n.selected = true;
if ((n.data as FilterListItem).label.toString().toLowerCase().indexOf(searchVal) > -1) {
const item = n.data as FilterListItem;
if (item.label.toString().toLowerCase().indexOf(searchVal) > -1 ||
this.matchesNumericValue(item, searchVal)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kdinev The only thing that comes to my mind regarding the performance is that matchesNumericValue is called a second time here when the ESF is hierarchical. Firstly, it is called inside the hierarchicalSelectMatches method when generating displayedListData and then when going through the tree nodes to expand parent nodes. What we can do is to create a set and populate it with the items and then match a record to the set so that we skip the second call. However, this is not a big deal since matchesNumericValue is just parsing some numbers but can add up if the data is big.

this.expandAllParentNodes(n);
}
});
} else {
this.displayedListData = this.esf.listData.filter((it, i) => (i === 0 && it.isSpecial) ||
(it.label !== null && it.label !== undefined) &&
!it.isBlanks &&
it.label.toString().toLowerCase().indexOf(searchVal) > -1);
(it.label.toString().toLowerCase().indexOf(searchVal) > -1 ||
this.matchesNumericValue(it, searchVal)));

this.esf.listData.forEach(i => i.isSelected = false);
this.displayedListData.forEach(i => i.isSelected = true);
Expand Down Expand Up @@ -724,7 +733,8 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy {
node.expanded = false;
}

if (element.label.toString().toLowerCase().indexOf(searchVal) > -1) {
if (element.label.toString().toLowerCase().indexOf(searchVal) > -1 ||
this.matchesNumericValue(element, searchVal)) {
element.isSelected = true;
this.hierarchicalSelectAllChildren(element);
this._hierarchicalSelectedItems.push(element);
Expand Down Expand Up @@ -802,6 +812,23 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy {
}
}

private matchesNumericValue(item: FilterListItem, searchVal: string): boolean {
const columnDataType = this.esf.column?.dataType;
if (typeof item.value !== 'number' ||
(columnDataType !== GridColumnDataType.Number &&
columnDataType !== GridColumnDataType.Currency &&
columnDataType !== GridColumnDataType.Percent)) {
return false;
}

let numericValue = item.value;
if (columnDataType === GridColumnDataType.Percent) {
numericValue = parseFloat((item.value * 100).toPrecision(15));
}

return numericValue.toString().toLowerCase().indexOf(searchVal) > -1;
Comment on lines +824 to +829
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matchesNumericValue() compares against numericValue.toString(), which drops trailing zeros and can produce scientific notation. As a result, valid numeric inputs that the search field allows (e.g. 1000.00, 1e5) may still fail to match locale-formatted labels like 1,000.00 even though they represent the same value. Consider normalizing searchVal (e.g. parse to a number and stringify) and/or comparing against a canonical non-localized numeric string so decimal inputs reliably match.

Suggested change
let numericValue = item.value;
if (columnDataType === GridColumnDataType.Percent) {
numericValue = parseFloat((item.value * 100).toPrecision(15));
}
return numericValue.toString().toLowerCase().indexOf(searchVal) > -1;
// Normalize the search value: remove grouping separators, trim, and parse as number.
const normalizedSearch = searchVal ? searchVal.replace(/,/g, '').trim() : '';
if (!normalizedSearch) {
return false;
}
const parsedSearch = Number(normalizedSearch);
if (Number.isNaN(parsedSearch)) {
return false;
}
let numericValue = item.value;
if (columnDataType === GridColumnDataType.Percent) {
numericValue = parseFloat((item.value * 100).toPrecision(15));
}
const canonicalItem = numericValue.toString();
const canonicalSearch = parsedSearch.toString();
return canonicalItem.toLowerCase().indexOf(canonicalSearch.toLowerCase()) > -1;

Copilot uses AI. Check for mistakes.
}

private onArrowUpKeyDown() {
if (this.focusedItem && this.focusedItem.index === 0 && this.virtDir.state.startIndex === 0) {
// on ArrowUp the focus stays on the same element if it is the first focused
Expand Down
19 changes: 19 additions & 0 deletions projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4113,6 +4113,25 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => {
expect(listItems.length).toBe(8, 'incorrect rendered list items count');
});

it('Should match numeric column values when searching without locale-specific formatting characters.', async () => {
GridFunctions.clickExcelFilterIconFromCodeAsync(fix, grid, 'Downloads');
fix.detectChanges();
await wait(100);
const searchComponent = GridFunctions.getExcelStyleSearchComponent(fix);
const inputNativeElement = GridFunctions.getExcelStyleSearchComponentInput(fix, searchComponent);

// Type 1000 (without thousands separator) in search box.
// The value 1000 is displayed as "1,000" in the ESF list due to locale formatting.
// Searching "1000" should still match the "1,000" entry.
UIInteractions.clickAndSendInputElementValue(inputNativeElement, '1000', fix);
fix.detectChanges();
await wait(100);

const listItems = GridFunctions.getExcelStyleSearchComponentListItems(fix, searchComponent);
// Expect 3 items: Select All + Add to current filter + the matched "1,000" item
expect(listItems.length).toBe(3, 'searching plain number should match locale-formatted label');
});

it('Should enable/disable the apply button correctly.', async () => {
GridFunctions.clickExcelFilterIconFromCodeAsync(fix, grid, 'ProductName');
fix.detectChanges();
Expand Down
151 changes: 78 additions & 73 deletions src/app/grid-cellMerging/grid-cellMerging.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,79 +43,84 @@ <h4 class="sample-title">Grid with cell merge</h4>
}
</igx-input-group>
</div>
<igx-grid [data]="data" width="800px" height="550px" [moving]="true" #grid1
[cellMergeMode]="'always'" [rowSelection]="'single'">
<igx-column field="OrderID" header="Order ID">
<ng-template igxCell let-cell="cell" let-val>
Value: {{val}},Index: {{cell.row.index}}
</ng-template>
</igx-column>
<igx-column field="ShipCountry" [groupable]="true" [hasSummary]="true" header="Ship Country" [merge]="true" width="200px" sortable="true">
</igx-column>
<igx-column field="OrderDate" header="Order Date" width="200px" [merge]="true" [groupable]="true" [dataType]="'date'" sortable="true">
</igx-column>
<igx-column field="PostalCode" header="Postal Code" width="200px" >
</igx-column>
<igx-column field="Discontinued" header="Discontinued" width="200px" sortable="true">
</igx-column>
<igx-column field="ShipName" header="Ship Name" width="250px">
</igx-column>
<igx-column field="ShipCity" header="Ship City" width="250px">
</igx-column>
<igx-column field="ShipperName" header="Shipper Name" width="250px">
</igx-column>
<igx-column field="Salesperson" header="Salesperson" width="250px">
</igx-column>
<igx-column field="UnitPrice" header="Unit Price" width="150px" dataType="number">
</igx-column>
<igx-column field="Quantity" header="Quantity" width="150px" dataType="number">
</igx-column>

<igx-action-strip>
<igx-grid-pinning-actions></igx-grid-pinning-actions>
</igx-action-strip>
<igx-grid-toolbar>
<igx-grid-toolbar-actions>
<igx-grid-toolbar-hiding></igx-grid-toolbar-hiding>
<igx-grid-toolbar-pinning></igx-grid-toolbar-pinning>
</igx-grid-toolbar-actions>
</igx-grid-toolbar>
</igx-grid>
<div class="grids-grid">
<igx-grid [data]="data" height="550px" [moving]="true" #grid1
[cellMergeMode]="'always'" [rowSelection]="'single'" [allowFiltering]="true" [filterMode]="'excelStyleFilter'">
<igx-column field="OrderID" header="Order ID">
<ng-template igxCell let-cell="cell" let-val>
Value: {{val}},Index: {{cell.row.index}}
</ng-template>
</igx-column>
<igx-column field="ShipCountry" [groupable]="true" [hasSummary]="true" header="Ship Country" [merge]="true" width="200px" sortable="true">
</igx-column>
<igx-column field="OrderDate" header="Order Date" width="200px" [merge]="true" [groupable]="true" [dataType]="'date'" sortable="true">
</igx-column>
<igx-column field="PostalCode" header="Postal Code" width="200px" >
</igx-column>
<igx-column field="Discontinued" header="Discontinued" width="200px" sortable="true">
</igx-column>
<igx-column field="ShipName" header="Ship Name" width="250px">
</igx-column>
<igx-column field="ShipCity" header="Ship City" width="250px">
</igx-column>
<igx-column field="ShipperName" header="Shipper Name" width="250px">
</igx-column>
<igx-column field="Salesperson" header="Salesperson" width="250px">
</igx-column>
<igx-column field="UnitPrice" header="Unit Price" width="150px" dataType="currency">
</igx-column>
<igx-column field="ExtendedPrice" header="Extended Price" width="150px" dataType="currency">
</igx-column>
<igx-column field="Quantity" header="Quantity" width="150px" dataType="number">
</igx-column>

<h4 class="sample-title">Hierarchical grid with cell merge</h4>
<igx-action-strip>
<igx-grid-pinning-actions></igx-grid-pinning-actions>
</igx-action-strip>
<igx-grid-toolbar>
<igx-grid-toolbar-actions>
<igx-grid-toolbar-hiding></igx-grid-toolbar-hiding>
<igx-grid-toolbar-pinning></igx-grid-toolbar-pinning>
</igx-grid-toolbar-actions>
</igx-grid-toolbar>
</igx-grid>

<igx-hierarchical-grid #hierarchicalGrid width="800px" [rowSelection]="'single'" [height]="'550px'" [data]="hierarchicalData" [autoGenerate]="false"
[allowAdvancedFiltering]="true" [cellMergeMode]="'always'">
<igx-grid-toolbar></igx-grid-toolbar>
<igx-column field="EmployeeID" width="200px" [hidden]="true">
</igx-column>
<igx-column field="FirstName" width="200px">
</igx-column>
<igx-column field="LastName" width="200px">
</igx-column>
<igx-column field="Title" width="200px" [merge]="true" [hasSummary]="true">
</igx-column>
<igx-column [groupable]="true" [hasSummary]="true" field="City" width="200px" [merge]="true" editable="true" sortable="true">
</igx-column>
<igx-row-island [key]="'Orders'" [cellMergeMode]="'always'">
<igx-column field="CustomerID" width="200px">
</igx-column>
<igx-column field="ShipName" width="200px">
</igx-column>
<igx-column field="ShipCountry" width="200px" [merge]="true" sortable="true">
</igx-column>
<igx-column field="ShipCity" width="200px" [merge]="true" sortable="true">
</igx-column>
</igx-row-island>
</igx-hierarchical-grid>
<igx-hierarchical-grid #hierarchicalGrid [rowSelection]="'single'" [height]="'550px'" [data]="hierarchicalData" [autoGenerate]="false"
[allowAdvancedFiltering]="true" [cellMergeMode]="'always'">
<igx-grid-toolbar [title]="'Hierarchical grid with cell merging'"></igx-grid-toolbar>
<igx-column field="EmployeeID" width="200px" [hidden]="true">
</igx-column>
<igx-column field="FirstName" width="200px">
</igx-column>
<igx-column field="LastName" width="200px">
</igx-column>
<igx-column field="Title" width="200px" [merge]="true" [hasSummary]="true">
</igx-column>
<igx-column [groupable]="true" [hasSummary]="true" field="City" width="200px" [merge]="true" editable="true" sortable="true">
</igx-column>
<igx-row-island [key]="'Orders'" [cellMergeMode]="'always'">
<igx-column field="CustomerID" width="200px">
</igx-column>
<igx-column field="ShipName" width="200px">
</igx-column>
<igx-column field="ShipCountry" width="200px" [merge]="true" sortable="true">
</igx-column>
<igx-column field="ShipCity" width="200px" [merge]="true" sortable="true">
</igx-column>
</igx-row-island>
</igx-hierarchical-grid>

<h4 class="sample-title">Tree grid with cell merge</h4>
<button (click)="toggleStrategy()">Toggle strategy</button>
<igx-tree-grid [autoGenerate]="false" [rowSelection]="'single'" [data]="treeData" [cellMergeMode]="'always'" [mergeStrategy]="treeGridMergeStrategy"
childDataKey="ChildCompanies" primaryKey="ID" [expansionDepth]="1" width="800px" [height]="'550px'">
<igx-column field="ContactName" width="200px" [hasSummary]="true">
</igx-column>
<igx-column field="ContactTitle" width="200px"></igx-column>
<igx-column field="Country" width="200px" [merge]="true" sortable="true"></igx-column>
<igx-column field="City" width="200px" [merge]="true" sortable="true"></igx-column>
</igx-tree-grid>
<igx-tree-grid [autoGenerate]="false" [rowSelection]="'single'" [data]="treeData" [cellMergeMode]="'always'" [mergeStrategy]="treeGridMergeStrategy"
childDataKey="ChildCompanies" primaryKey="ID" [expansionDepth]="1" [height]="'550px'">
<igx-grid-toolbar [title]="'Tree grid with cell merging'">
<igx-grid-toolbar-actions>
<button igxButton (click)="toggleStrategy()">Toggle strategy</button>
</igx-grid-toolbar-actions>
</igx-grid-toolbar>
<igx-column field="ContactName" width="200px" [hasSummary]="true">
</igx-column>
<igx-column field="ContactTitle" width="200px"></igx-column>
<igx-column field="Country" width="200px" [merge]="true" sortable="true"></igx-column>
<igx-column field="City" width="200px" [merge]="true" sortable="true"></igx-column>
</igx-tree-grid>
</div>
6 changes: 6 additions & 0 deletions src/app/grid-cellMerging/grid-cellMerging.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@
.searchInput{
width: 800px;
}

.grids-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(800px, 1fr));
gap: 1rem;
}
7 changes: 5 additions & 2 deletions src/app/grid-cellMerging/grid-cellMerging.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FormsModule } from '@angular/forms';
import {
DefaultTreeGridMergeStrategy,
IgxActionStripComponent,
IgxButtonDirective,
IgxCellTemplateDirective,
IgxColumnComponent,
IgxGridComponent,
Expand All @@ -12,10 +13,10 @@ import {
IgxGridToolbarHidingComponent,
IgxGridToolbarPinningComponent,
IgxHierarchicalGridComponent,
IgxIconButtonDirective,
IgxIconComponent,
IgxInputDirective,
IgxInputGroupComponent,
IgxPaginatorComponent,
IgxPrefixDirective,
IgxRowIslandComponent,
IgxSuffixDirective,
Expand Down Expand Up @@ -50,7 +51,9 @@ import { INVOICE_DATA } from '../shared/invoiceData';
IgxSuffixDirective,
IgxIconComponent,
IgxInputDirective,
IgxCellTemplateDirective
IgxCellTemplateDirective,
IgxButtonDirective,
IgxIconButtonDirective
]
})
export class GridCellMergingComponent {
Expand Down
4 changes: 2 additions & 2 deletions src/app/grid-column-types/grid-column-types.sample.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[filterMode]="filterMode"
rowSelection="multiple"
[rowEditable]="false"
width="700px"
width="800px"
[primaryKey]="'ID'">

<igx-grid-toolbar >
Expand Down Expand Up @@ -45,7 +45,7 @@
[filterMode]="filterMode"
rowSelection="multiple"
[rowEditable]="true"
[width]="'700px'">
[width]="'800px'">

<igx-grid-toolbar >
<igx-grid-toolbar-actions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export class HierarchicalGridRemoteSampleComponent implements OnInit, AfterViewI
fields: [
{ field: 'orderId', dataType: 'number' }, // first field will be treated as foreign key
{ field: 'productId', dataType: 'number' },
{ field: 'unitPrice', dataType: 'number' },
{ field: 'unitPrice', dataType: 'currency' },
{ field: 'quantity', dataType: 'number' },
{ field: 'discount', dataType: 'number' }
{ field: 'discount', dataType: 'percent' }
]
}
]
Expand Down Expand Up @@ -131,7 +131,7 @@ export class HierarchicalGridRemoteSampleComponent implements OnInit, AfterViewI
console.log('data', data);
event.grid.data = Object.values(data);
event.grid.isLoading = false;
this.cdr.detectChanges();
this.cdr.detectChanges();
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@
<button igxButton="contained" (click)="logTransactionsIsland1()">RI1 Transactions</button>
<button igxButton="contained" (click)="commitTransactionsIsland1()">Commit R1 Transactions</button>
<div class="wrapper">
<igx-hierarchical-grid #grid1 [batchEditing]="true" [data]="remoteData" [rowEditable]="true" [primaryKey]="'CustomerID'" [autoGenerate]="false"
[height]="'800px'" [width]="'100%'" #hGrid>
<igx-column field="CustomerID"></igx-column>
<igx-column field="CompanyName"></igx-column>
<igx-column field="ContactName"></igx-column>
<igx-column field="ContactTitle"></igx-column>
<igx-column field="Country"></igx-column>
<igx-column field="Phone"></igx-column>
<igx-row-island #rowIsland1 [key]="'Orders'" [primaryKey]="'OrderID'" [autoGenerate]="false"
[rowSelection]='selectionMode' (gridCreated)="gridCreated($event, rowIsland1)"
[rowEditable]="true">
<igx-column field="OrderID"></igx-column>
<igx-column field="OrderDate" [dataType]='"date"'></igx-column>
<igx-column field="ShipCountry"></igx-column>
<igx-column field="ShipCity"></igx-column>
<igx-column field="ShipAddress"></igx-column>
<igx-row-island #rowIsland2 [key]="'Order_Details'" [height]="'300px'" [primaryKey]="'ProductID'"
[autoGenerate]="false" [rowSelection]='selectionMode' (gridCreated)="gridCreated($event, rowIsland2)">
<igx-column field="ProductID"></igx-column>
<igx-column field="UnitPrice"></igx-column>
<igx-column field="Quantity"></igx-column>
<igx-column field="Discount"></igx-column>
</igx-row-island>
</igx-row-island>
</igx-hierarchical-grid>
</div>
<igx-hierarchical-grid #grid1 [batchEditing]="true" [data]="remoteData" [rowEditable]="true" [primaryKey]="'CustomerID'" [autoGenerate]="false"
[height]="'800px'" [width]="'100%'" #hGrid>
<igx-column field="CustomerID"></igx-column>
<igx-column field="CompanyName"></igx-column>
<igx-column field="ContactName"></igx-column>
<igx-column field="ContactTitle"></igx-column>
<igx-column field="Country"></igx-column>
<igx-column field="Phone"></igx-column>
<igx-row-island #rowIsland1 [key]="'Orders'" [primaryKey]="'OrderID'" [autoGenerate]="false"
[rowSelection]="selectionMode" (gridCreated)="gridCreated($event, rowIsland1)"
[rowEditable]="true">
<igx-column field="OrderID"></igx-column>
<igx-column field="OrderDate" [dataType]="'date'"></igx-column>
<igx-column field="ShipCountry"></igx-column>
<igx-column field="ShipCity"></igx-column>
<igx-column field="ShipAddress"></igx-column>
<igx-row-island #rowIsland2 [key]="'Order_Details'" [height]="'300px'" [primaryKey]="'ProductID'"
[autoGenerate]="false" [rowSelection]="selectionMode" (gridCreated)="gridCreated($event, rowIsland2)">
<igx-column field="ProductID"></igx-column>
<igx-column field="UnitPrice" [dataType]="'currency'"></igx-column>
<igx-column field="Quantity"></igx-column>
<igx-column field="Discount" [dataType]="'percent'"></igx-column>
</igx-row-island>
</igx-row-island>
</igx-hierarchical-grid>
</div>
Loading
Loading