Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2c3168d
feat: Added virtual scroll component and sample implementation
rkaraivanov May 18, 2026
e7ce7a1
fix: ng-packagr build errors for virtual scroll component
rkaraivanov May 18, 2026
5aa53cf
chore: update virtual scroll component spec
rkaraivanov May 18, 2026
90dc5ec
fix: fixes the issue with the virtual scroll component not updating t…
rkaraivanov May 18, 2026
fe32fac
feat: Use Binary Indexed Tree for the scroll engine
rkaraivanov May 22, 2026
b987e22
chore: Lint error in scroll engine
rkaraivanov May 22, 2026
1e3de56
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov May 26, 2026
0c240ea
fix: Adjusted dataRequest event test
rkaraivanov May 26, 2026
1d87ba5
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jun 2, 2026
f7dd940
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jun 10, 2026
042c5e4
feat: Added RTL support for horizontal virtual scroll
rkaraivanov Jun 10, 2026
d0e4b77
fix: fix virtual scroll component spec test
rkaraivanov Jun 11, 2026
242dd44
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jun 18, 2026
1fed9b5
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jun 18, 2026
ed2aa0a
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jun 24, 2026
133e4e0
refactor: Skip zone re-entry when scrolling
rkaraivanov Jun 24, 2026
ebd89f9
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jun 25, 2026
17adfaf
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jun 30, 2026
1fc98be
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Jul 2, 2026
fe1917c
Merge branch 'master' into rkaraivanov/virtual-scroll-component
ChronosSF Jul 28, 2026
65119a7
Merge branch 'rkaraivanov/virtual-scroll-component' of https://github…
rkaraivanov Aug 31, 2026
356753d
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Aug 31, 2026
f23bac2
feat: align with the web components implementation
rkaraivanov Aug 31, 2026
459758c
Merge branch 'master' into rkaraivanov/virtual-scroll-component
simeonoff Sep 8, 2026
6c853b6
refactor(virtual-scroll): restructure structural CSS conventions
simeonoff Sep 8, 2026
65f9300
Merge branch 'master' into rkaraivanov/virtual-scroll-component
viktorkombov Sep 8, 2026
c0f421c
Merge branch 'master' into rkaraivanov/virtual-scroll-component
viktorkombov Sep 14, 2026
3b63af6
Merge branch 'master' into rkaraivanov/virtual-scroll-component
simeonoff Sep 15, 2026
fc40c26
refactor(ESF, combo): use IgxVirtualScrollComponent instead of igxFor…
viktorkombov Sep 16, 2026
1638ed2
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Sep 16, 2026
277d80a
chore: remove duplicate import of 'signal' from '@angular/core'
rkaraivanov Sep 16, 2026
a6769df
Merge branch 'master' into rkaraivanov/virtual-scroll-component
ChronosSF Sep 17, 2026
6d390f0
test(virtual-scroll): failing specs for nearest align and runtime dir…
ChronosSF Sep 17, 2026
547ef1c
test(*): failing specs for compression tail, remeasure and header skip
ChronosSF Sep 17, 2026
1c8cf2c
Merge remote-tracking branch 'origin/master' into rkaraivanov/virtual…
rkaraivanov Sep 18, 2026
5d9bf7a
Fix virtual scroll review findings
rkaraivanov Sep 18, 2026
d5139b9
Merge branch 'master' into rkaraivanov/virtual-scroll-component
rkaraivanov Sep 18, 2026
98bfa2b
Fix navigateToChildGrid ending before its child renders
rkaraivanov Sep 18, 2026
6d6f455
Merge branch 'rkaraivanov/virtual-scroll-component' of https://github…
rkaraivanov Sep 18, 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
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Here are the best practices and the style guide information.

Here is a link to the most recent Angular style guide https://angular.dev/style-guide

### CSS naming convention

See [`css-naming-convention.md`](../css-naming-convention.md) for the component CSS class naming rules — CUBE CSS for new/actively-migrated components, BEM for the rest of the existing library.

### TypeScript Best Practices

- Use strict type checking
Expand Down
27 changes: 25 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,35 @@ All notable changes for each version of this project will be documented in this

### New Features

- `IgxVirtualScrollComponent`
- Added `initialViewportSize`, the viewport size to render the first window against. A list that is hidden until the change detection pass that reveals it has no size to measure in that pass and would render nothing; this gives that first render a size to work from, and the host's own size takes over once it has been laid out.
- Added `dataWindow`, taking a loaded page of a larger collection as `{ items, startIndex, totalCount }`. The list is as long as `totalCount`, so the scrollbar spans the whole collection while only the page is in memory, and indices the page does not cover render nothing until a page that covers them arrives. `data` is unchanged and is used whenever `dataWindow` is not set.

- **Forms**
- `igxInput`, `igx-checkbox`, `igx-switch`, `igx-radio-group`, `igx-select`, `igx-combo`, `igx-simple-combo`, `igx-date-picker`, `igx-time-picker` and `igx-date-range-picker` now work with Angular Signal Forms (`[formField]`). Validity, touched, dirty, disabled and required state are read from the signal-backed control.
- `igx-checkbox`, `igx-switch` and `igx-radio-group` now report `required` and `aria-required` for `Validators.requiredTrue`, as `igxInput` already did.
- `igx-radio-group` implements `setDisabledState`, so `control.disable()` / `enable()` and the Signal Forms `disabled` rule reach the radio buttons. Buttons disabled in the template stay disabled after `enable()`.
- `IgcFormControlDirective`
- Added support for `igc-color-picker` so it can be bound with `ngModel` and `formControlName`, in the same way `igc-rating` is already supported.

### General

- The Excel style filtering search list, `IgxComboComponent` and `IgxSimpleComboComponent` are now virtualized by `IgxVirtualScrollComponent` instead of the `igxFor` directive. A row is measured in the DOM once it renders and the measured size replaces the estimate it started from; rows that have not rendered keep that estimate.
- The list markup changed accordingly: `igx-display-container` and the `igx-vhelper--vertical` scrollbar are replaced by the `igx-virtual-scroll` host and its `igx-virtual-item` row wrappers. Applications and tests that reach into those elements directly need updating.
- `IgxDropDownComponent` accepts a content-projected `igx-virtual-scroll` in addition to `*igxFor`, which keeps working as documented. Selection and navigation behave the same either way.

### Breaking Changes
- **Combo** - `IgxComboComponent.virtualScrollContainer` and `IgxSimpleComboComponent.virtualScrollContainer`, both `@hidden @internal`, are now an `IgxVirtualScrollComponent` instead of an `IgxForOfDirective`, and the Excel style filtering search list has no `virtDir` anymore. The public `virtualizationState` and `totalItemCount` are unchanged.

### Bug Fixes

- **Accessibility**
- Removed the nested list role from the internal virtual-scroll containers in Combo, Simple Combo and Excel-style filtering, preserving their existing listboxes and options.
- `IgxDropDownComponent`
- Navigation and item lookup now use the same normalized `dataWindow` indices and total count as the projected virtual scroll, including fractional or non-finite metadata and pages extending past the declared total.
- `IgxComboComponent`, `IgxSimpleComboComponent`
- Fixed remote pages changing position before their replacements arrive and redundant requests for an already loaded initial range. Changes to a positive `totalItemCount` refresh the list without rebinding data; a reduced total excludes out-of-range records before filtering and grouping.
- Reduced selection-resolution work during change detection. Each combo resolves its selection once per check and validates cached primitive-key matches before reusing them. Missing or invalid matches share one fallback scan; object keys retain deep-equality matching. In-place changes that create an earlier duplicate of a cached key are not detected without rebinding data.

### Breaking Changes

Expand All @@ -20,8 +45,6 @@ All notable changes for each version of this project will be documented in this

### New Features

- `IgcFormControlDirective`
- Added support for `igc-color-picker` so it can be bound with `ngModel` and `formControlName`, in the same way `igc-rating` is already supported.

- `IgxChipComponent`
- Added the `outlined` property to the component. When set to `true`, the Chip will have an outlined style.
Expand Down
26 changes: 24 additions & 2 deletions css-naming-convention.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
# Ignite UI for Angular CSS Naming Convention

Ignite UI for Angular uses the Two Dashes style of the [BEM naming convention](https://en.bem.info/methodology/naming-convention/) for CSS classes.
## Going forward: CUBE CSS

New and actively-migrated components follow a [CUBE CSS](https://cube.fyi)-inspired strategy instead of BEM. CUBE splits styles into four concerns:

- **Composition** — layout-only rules that arrange a component's own internal structure (e.g. the wrapper elements a component renders around projected content). These stay component-scoped for now; the library does not yet have a shared, cross-component composition/layout utility layer.
- **Utility** — single-purpose, reusable classes for one CSS concern at a time. Introduce these only where a genuine cross-component need exists; don't invent component-scoped "utility" classes as a substitute for Block/Exception naming.
- **Block** — a component's own root class and any other independently addressable "thing" it renders (e.g. a directive-backed item wrapper). Blocks are flat class names: no `--`-style modifier chains. Where an element is purely an internal part of another block (not independently meaningful on its own), name it with the block as a prefix followed by `__` (e.g. `igx-virtual-scroll__track`) so its ownership is clear at a glance — this is a plain naming convention, not an invocation of BEM's element/modifier tooling (no `bem` Sass mixins, no `--` modifiers).
- **Exception** — state and variants. Prefer `data-*` attributes reflecting a component's own input/state (e.g. `[data-orientation="horizontal"]`) over boolean modifier classes kept in sync by the component. Attributes are also naturally queryable and stylable without extra host-binding wiring.

Rules for CUBE-based components:

- Class names are lower-case, hyphen-separated words, prefixed with `igx-` (unchanged from before).
- No `--` modifier classes for state that varies at runtime — use a `data-*` attribute instead.
- No dependency on the `igniteui-theming/sass/bem` mixins (`b()`/`e()`/`m()`) — write plain selectors.
- If the component has no design tokens (no color/typography/elevation surface of its own), it does not need a schema entry in `igniteui-theming` — only the structural `themes/_base.scss` file, wrapped in the shared `layer(base)` mixin so it still participates in the library's `ig.base` cascade layer.

This does not retroactively apply to existing components — see below.

## Existing components: BEM (Two Dashes style)

The rest of the library (everything not explicitly migrated) uses the [BEM naming convention](https://en.bem.info/methodology/naming-convention/), Two Dashes style, generated via the `igniteui-theming/sass/bem` mixins (`b()`, `e()`, `m()`):

Naming Convention:

Expand All @@ -12,4 +32,6 @@ ex.: `block-name__elem-name--mod-name`
- Boolean modifiers are delimited by double hyphens (--).
- Key-value type modifiers are not used.

**Important!** Double hyphen within the comment (--) is perceived as part of the comment and therefore its presence lead to error during document validation. [HTML5 Specification](http://www.w3.org/TR/html5/syntax.html#comments)
**Important!** Double hyphen within the comment (--) is perceived as part of the comment and therefore its presence lead to error during document validation. [HTML5 Specification](http://www.w3.org/TR/html5/syntax.html#comments)

Existing BEM-based components are not required to migrate to CUBE. Only migrate a component's naming when you are already doing substantial, deliberate work on its styles — don't rename classes as a drive-by change.
19 changes: 19 additions & 0 deletions projects/igniteui-angular/combo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public dataLoading(evt): void {
What the combo exposes is a `virtualizationState` property that gives state of the combo - first index and the number of items that needs to be loaded.
The service, should inform the combo for the total items that are on the server - using the `totalItemCount` property.

The first rendered range does not emit `dataPreLoad` when the initial page already covers it.
Later range changes still emit the event, so the consumer can cancel a superseded request.
Capture the requested range when starting a fetch and cancel its subscription before replacing
it; assigning `data` does not identify which request produced the page.

Changing a positive `totalItemCount` refreshes the list without rebinding `data`. If the total
shrinks, loaded records beyond it are excluded while the valid prefix keeps its position.
This limit is applied before filtering and grouping; group headers are not remote records.


## Features

Expand Down Expand Up @@ -86,6 +95,16 @@ export class MyCombo {
}
```

With primitive value keys, resolving the selection reuses previously found records
after verifying their index, identity and key. Missing or invalid matches share one
fallback scan. Changing the data array, its length or `valueKey` clears the lookup;
object keys retain deep-equality matching. A selected key whose record is not loaded
is represented by `{ [valueKey]: key }` and is checked again on the next resolution.

The hit validation does not detect a different record becoming an earlier duplicate
of an already cached key without rebinding the data. This remains a known limitation
of the lookup, not a guarantee of compatibility with every in-place mutation.

### Selection Events

The `igx-combo` exposes both `selectionChanging` and `selectionChanged`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I

/** @hidden @internal */
public override get scrollContainer(): HTMLElement {
// TODO: Update, use public API if possible:
return this.virtDir.dc.location.nativeElement;
}

protected get isScrolledToLast(): boolean {
const scrollTop = this.virtDir.scrollPosition;
const scrollHeight = this.virtDir.getScroll()!.scrollHeight;
return Math.floor(scrollTop + this.virtDir.igxForContainerSize) === scrollHeight;
return this.virtualization!.scrollElement;
}

protected get lastVisibleIndex(): number {
Expand Down Expand Up @@ -137,15 +130,19 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
* @hidden
*/
public override navigateFirst() {
this.navigateItem(this.virtDir.igxForOf!.findIndex(e => !e?.isHeader));
// The first selectable entry can only be looked for in what is loaded. A page that
// starts further in does not hold it, so the collection's own start is the target.
this.navigateItem(this.virtualization?.startIndex === 0
? this.virtualization.findIndex(e => !e?.isHeader)
: 0);
this.combo.setActiveDescendant();
}

/**
* @hidden
*/
public override navigatePrev() {
if (this._focusedItem && this._focusedItem.index === 0 && this.virtDir.state.startIndex === 0) {
if (this._focusedItem && this._focusedItem.index === 0) {
this.combo.focusSearchInput(false);
this.focusedItem = null;
} else {
Expand All @@ -159,7 +156,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
* @hidden
*/
public override navigateNext() {
const lastIndex = this.combo.totalItemCount ? this.combo.totalItemCount - 1 : this.virtDir.igxForOf!.length - 1;
const lastIndex = (this.virtualization?.length ?? 0) - 1;
if (this._focusedItem && this._focusedItem.index === lastIndex) {
this.focusAddItemButton();
} else {
Expand All @@ -185,7 +182,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
* @hidden @internal
*/
public override updateScrollPosition() {
this.virtDir.getScroll()!.scrollTop = this._scrollPosition;
this.virtualization!.scrollPosition = this._scrollPosition;
}

/**
Expand All @@ -208,14 +205,15 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
}

public override ngAfterViewInit() {
this.virtDir.getScroll()!.addEventListener('scroll', this.scrollHandler);
super.ngAfterViewInit();
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
}

/**
* @hidden @internal
*/
public override ngOnDestroy(): void {
this.virtDir.getScroll()!.removeEventListener('scroll', this.scrollHandler);
this.virtualization?.scrollElement.removeEventListener('scroll', this.scrollHandler);
super.ngOnDestroy();
}

Expand Down
Loading
Loading