Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ All notable changes for each version of this project will be documented in this

### Behavioral Changes

- `IgxComboComponent`, `IgxSimpleComboComponent`, `IgxDropDownComponent` and `IgxSelectComponent`
- The components, their items and groups now use `ChangeDetectionStrategy.OnPush`. Properties set from code still update the view, and combo records mutated in place still render on the next host check.
- **Theming** - Scrollbar arrow buttons cannot be styled or enabled through the standard properties, and `scrollbar-width: thin` removes them where the platform draws them.
- **Firefox** - The `scrollbar-color` and `scrollbar-width` properties are not supported on Firefox versions prior to 64, so the scrollbars in those versions will render with the platform default colors and size.

Expand Down
111 changes: 111 additions & 0 deletions my-changes.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
diff --git a/projects/igniteui-angular-performance/src/app/app.routes.ts b/projects/igniteui-angular-performance/src/app/app.routes.ts
index 5804152b12..2ae1c41a32 100644
--- a/projects/igniteui-angular-performance/src/app/app.routes.ts
+++ b/projects/igniteui-angular-performance/src/app/app.routes.ts
@@ -3,8 +3,15 @@ import { GridComponent } from './grid/grid.component';
import { TreeGridComponent } from './tree-grid/tree-grid.component';
import { PivotGridComponent } from './pivot-grid/pivot-grid.component';
import { HierarchicalGridComponent } from './hierarchical-grid/hierarchical-grid.component';
+import { ComboGridComponent } from './combo-grid/combo-grid.component';

export const routes: Routes = [
+ {
+ path: "combo-grid-1m",
+ title: "Combo & Grid 1M records",
+ component: ComboGridComponent,
+ data: { rows: 1_000_000 }
+ },
{
path: "pivot-grid",
title: "Pivot Grid",
diff --git a/projects/igniteui-angular/drop-down/src/drop-down/drop-down-virtualization.ts b/projects/igniteui-angular/drop-down/src/drop-down/drop-down-virtualization.ts
index ac0557e6f7..d661b2fb4f 100644
--- a/projects/igniteui-angular/drop-down/src/drop-down/drop-down-virtualization.ts
+++ b/projects/igniteui-angular/drop-down/src/drop-down/drop-down-virtualization.ts
@@ -68,17 +68,10 @@ export function createDropDownVirtualization(
class VirtualScrollVirtualization implements IgxDropDownVirtualization {
private readonly _disconnect = new Subject<void>();

- /** The window last rendered, for answering whether an index has an element. */
- private _rendered = { startIndex: 0, endIndex: -1 };
-
constructor(
private _scroll: IgxVirtualScrollComponent<any>,
private _ref: ElementRef<HTMLElement>
- ) {
- outputToObservable(this._scroll.stateChange)
- .pipe(takeUntil(this._disconnect))
- .subscribe(state => this._rendered = state);
- }
+ ) { }

public get length(): number {
const window = this._scroll.dataWindow();
@@ -115,8 +108,13 @@ class VirtualScrollVirtualization implements IgxDropDownVirtualization {
return found < 0 ? -1 : found + (window?.startIndex ?? 0);
}

+ /**
+ * The rendered rows are the authority. `stateChange` reports the range the viewport
+ * wants, which over a paged collection reaches past the rows that have arrived, so a
+ * cached copy of it would answer for indices that have no element.
+ */
public isIndexRendered(index: number): boolean {
- return index >= this._rendered.startIndex && index <= this._rendered.endIndex;
+ return !!this._ref.nativeElement.querySelector(`[data-vs-index="${index}"]`);
}

/**
diff --git a/projects/igniteui-angular/virtual-scroll/src/virtual-scroll/virtual-scroll.component.spec.ts b/projects/igniteui-angular/virtual-scroll/src/virtual-scroll/virtual-scroll.component.spec.ts
index e5354845d7..4b3f93fe55 100644
--- a/projects/igniteui-angular/virtual-scroll/src/virtual-scroll/virtual-scroll.component.spec.ts
+++ b/projects/igniteui-angular/virtual-scroll/src/virtual-scroll/virtual-scroll.component.spec.ts
@@ -1133,6 +1133,48 @@ describe('IgxVirtualScrollComponent', () => {
.toContain(`Item ${wanted.startIndex}`);
});

+ it('should report a page that lands inside an unchanged range', async () => {
+ // The list scrolls into a hole and reports the range it needs. The page that
+ // arrives fills that hole without moving anything: the wanted range, the
+ // viewport, and the total size all stay exactly as they were, because the rows
+ // measure at the estimate. The consumer naming rendered rows for assistive
+ // technology hears about them through this report and nothing else.
+ await bindWindow(windowHost.pageAt(0));
+ await windowScroll.scrollToIndex(400);
+ await settle(windowFixture, windowScroll);
+ expect(vsItems(windowFixture).length).toBe(0);
+
+ const wanted = windowHost.states.at(-1)!;
+ windowHost.states.length = 0;
+
+ const count = wanted.endIndex - wanted.startIndex + 1;
+ await bindWindow(windowHost.pageAt(wanted.startIndex, count));
+
+ expect(vsIndices(windowFixture)).toContain(wanted.startIndex);
+ expect(windowHost.states.length).toBeGreaterThan(0);
+ expect(windowHost.states.at(-1)!.startIndex).toBe(wanted.startIndex);
+ });
+
+ it('should report again after the data was cleared and reloaded unchanged', async () => {
+ // Clearing everything and reloading the same page lands the list back in the
+ // exact state it reported before the clear. The consumer heard the list go
+ // empty, so the reload has to be told even though nothing about it is new.
+ await bindWindow(windowHost.pageAt(0));
+ const before = windowHost.states.at(-1)!;
+
+ windowHost.window.set(null);
+ windowHost.items.set([]);
+ await settle(windowFixture, windowScroll);
+ expect(vsItems(windowFixture).length).toBe(0);
+ windowHost.states.length = 0;
+
+ await bindWindow(windowHost.pageAt(0));
+
+ expect(windowHost.states.length).toBeGreaterThan(0);
+ expect(windowHost.states.at(-1)!.startIndex).toBe(before.startIndex);
+ expect(windowHost.states.at(-1)!.endIndex).toBe(before.endIndex);
+ });
+
it('should report a moved range whose loaded part has not changed', async () => {
// Two loaded rows, and a viewport that reaches well past both of them. Moving
// one row down changes the range the consumer is being asked for, while the
7 changes: 7 additions & 0 deletions projects/igniteui-angular-performance/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ import { GridComponent } from './grid/grid.component';
import { TreeGridComponent } from './tree-grid/tree-grid.component';
import { PivotGridComponent } from './pivot-grid/pivot-grid.component';
import { HierarchicalGridComponent } from './hierarchical-grid/hierarchical-grid.component';
import { ComboGridComponent } from './combo-grid/combo-grid.component';

export const routes: Routes = [
{
path: "combo-grid-1m",
title: "Combo & Grid 1M records",
component: ComboGridComponent,
data: { rows: 1_000_000 }
},
{
path: "pivot-grid",
title: "Pivot Grid",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div class="combo-row">
<igx-combo
[data]="comboData"
[displayKey]="'name'"
[valueKey]="'id'"
[width]="'320px'"
[itemsMaxHeight]="400"
placeholder="Combo over the same {{ rows }} records"
>
</igx-combo>

<span>
The grid below filters with the Excel style menu. Open it on
<strong>UniqueValue</strong> to list {{ rows }} distinct entries.
</span>
</div>

<div class="grid-wrapper">
<igx-grid
#grid
[data]="data"
[allowFiltering]="true"
[filterMode]="'excelStyleFilter'"
[height]="'100%'"
[width]="'100%'"
>
@for (col of columns; track col) {
<igx-column
[field]="col.field"
[header]="col.header"
[sortable]="col.sortable"
[dataType]="col.dataType"
[width]="col.width"
[groupable]="col.groupable"
>
</igx-column>
}
</igx-grid>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
gap: 8px;
}

.combo-row {
display: flex;
align-items: center;
gap: 12px;
flex: 0 0 auto;
}

.grid-wrapper {
flex: 1 1 auto;
min-height: 0;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ChangeDetectionStrategy, Component, inject, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { GridColumnDataType, IGX_GRID_DIRECTIVES, IgxGridComponent } from 'igniteui-angular';
import { IgxComboComponent } from 'igniteui-angular';
import { DataService } from '../services/data.service';

/** One combo entry, kept small so a million of them stay affordable. */
interface ComboRecord {
id: number;
name: string;
}

@Component({
selector: 'app-combo-grid',
imports: [IGX_GRID_DIRECTIVES, IgxComboComponent],
templateUrl: './combo-grid.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './combo-grid.component.scss'
})
export class ComboGridComponent {
protected columns: any[] = [];
protected data: any[] = [];
protected comboData: ComboRecord[] = [];
protected rows: number;

private dataService = inject(DataService);
private activatedRoute = inject(ActivatedRoute);

@ViewChild(IgxGridComponent, { static: true })
public grid!: IgxGridComponent;

constructor() {
this.rows = this.activatedRoute.snapshot.data.rows;

this.data = this.dataService.generateData(this.rows);
// A value per row and no two alike, so opening the Excel filter on this column has
// to list the whole collection rather than a handful of repeated entries.
this.data.forEach((row, index) => row.UniqueValue = index);

this.comboData = Array.from({ length: this.rows }, (_, index) => ({
id: index,
name: `Item ${index}`
}));

this.columns = [
{ field: 'UniqueValue', dataType: GridColumnDataType.Number, sortable: true, width: 'auto', groupable: false },
{ field: 'Name', dataType: GridColumnDataType.String, sortable: true, width: 'auto', groupable: true },
{ field: 'AthleteNumber', dataType: GridColumnDataType.Number, sortable: true, width: 'auto', groupable: true },
{ field: 'CountryName', dataType: GridColumnDataType.String, sortable: true, width: 'auto', groupable: true },
{ field: 'Registered', dataType: GridColumnDataType.DateTime, sortable: true, width: 'auto', groupable: true },
{ field: 'Active', dataType: GridColumnDataType.Boolean, sortable: true, width: 'auto', groupable: true },
{ field: 'NetWorth', dataType: GridColumnDataType.Currency, sortable: true, width: 'auto', groupable: true },
{ field: 'SuccessRate', dataType: GridColumnDataType.Percent, sortable: true, width: 'auto', groupable: true },
{ field: 'Position', dataType: GridColumnDataType.String, sortable: true, width: 'auto', groupable: true }
];
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { IgxComboItemComponent } from './combo-item.component';
import { Component, HostBinding, ChangeDetectionStrategy } from '@angular/core';
import { Component, ChangeDetectionStrategy } from '@angular/core';

/**
* @hidden
*/
@Component({
selector: 'igx-combo-add-item',
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.igx-drop-down__item]': 'isDropDownItem'
},
providers: [{ provide: IgxComboItemComponent, useExisting: IgxComboAddItemComponent }],
})
export class IgxComboAddItemComponent extends IgxComboItemComponent {
@HostBinding('class.igx-drop-down__item')
public get isDropDownItem(): boolean {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, QueryList, OnDestroy, AfterViewInit, ContentChildren, Input, booleanAttribute, inject, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
import { Component, QueryList, OnDestroy, AfterViewInit, ContentChildren, Input, booleanAttribute, inject, signal, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
import { IgxComboBase, IGX_COMBO_COMPONENT } from './combo.common';
import { IgxComboAddItemComponent } from './combo-add-item.component';
import { IgxComboAPIService } from './combo.api';
Expand All @@ -13,16 +13,23 @@ import { DropDownActionKey, IDropDownBase, IGX_DROPDOWN_BASE, IgxDropDownCompone
styleUrl: '../../../drop-down/src/drop-down/drop-down.component.css',
encapsulation: ViewEncapsulation.None,
providers: [{ provide: IGX_DROPDOWN_BASE, useExisting: IgxComboDropDownComponent }],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [IgxToggleDirective]
})
export class IgxComboDropDownComponent extends IgxDropDownComponent implements IDropDownBase, OnDestroy, AfterViewInit {
public combo = inject<IgxComboBase>(IGX_COMBO_COMPONENT);
protected comboAPI = inject(IgxComboAPIService);
private readonly _singleMode = signal(false);

/** @hidden @internal */
@Input({ transform: booleanAttribute })
public singleMode = false;
public get singleMode(): boolean {
return this._singleMode();
}

public set singleMode(value: boolean) {
this._singleMode.set(value);
}

/**
* @hidden
Expand Down
Loading
Loading