Skip to content
Open
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ All notable changes for each version of this project will be documented in this

## 22.2.0

### 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-vs__item` row wrappers. Applications and tests that reach into those elements directly need updating.
- `IgxComboComponent.virtualScrollContainer` and `IgxSimpleComboComponent.virtualScrollContainer` are marked `@hidden @internal`; their concrete type follows the engine the combo uses.
- `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.

### 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.
Comment on lines +16 to +18
- `IgxChipComponent`
- Added the `outlined` property to the component. When set to `true`, the Chip will have an outlined style.

Expand All @@ -25,6 +35,12 @@ All notable changes for each version of this project will be documented in this

### 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.
- `IgxCheckboxComponent`
- Fixed the tick-mark icon rendering with the Indigo shape (rounded rect + custom path) inside CSS-scoped subtrees that use a different design system than the application's global theme, e.g. a `material`-themed widget nested inside an `indigo`-themed app. Both tick-mark variants are now always rendered and toggled purely via CSS (`@container style(--ig-theme: indigo)`), removing the dependency on JS-side theme detection that could go stale in nested/multi-theme scenarios (#15021).
- **Ripple**
Expand Down
9 changes: 9 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
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
120 changes: 97 additions & 23 deletions projects/igniteui-angular/combo/src/combo/combo.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ import {
getCurrentResourceStrings,
onResourceChangeHandle
} from 'igniteui-angular/core';
import { IForOfState, IgxForOfDirective } from 'igniteui-angular/directives';
import { IForOfState } from 'igniteui-angular/directives';
import { IgxVirtualScrollComponent, VirtualScrollState } from 'igniteui-angular/virtual-scroll';
import { IgxIconService } from 'igniteui-angular/icon';
import { IGX_INPUT_GROUP_TYPE, IgxInputDirective, IgxInputGroupComponent, IgxInputGroupType, IgxInputState, IgxHintDirective, IgxLabelDirective, IgxPrefixDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
import { IgxComboDropDownComponent } from './combo-dropdown.component';
Expand Down Expand Up @@ -90,6 +91,9 @@ export interface IgxComboBase {

let NEXT_ID = 0;

/** Row height assumed before a real row has been measured, in pixels. */
const DEFAULT_ITEM_SIZE = 40;


/** @hidden @internal */
export const enum DataTypes {
Expand Down Expand Up @@ -335,6 +339,7 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
// during filtering & selection for the igx-simple-combo
// since the simple combo's input is both a container for the selection and a filter
this._data = (val) ? val.filter(x => x !== undefined) : [];
this._loadedStartIndex = this._virtualizationState.startIndex ?? 0;
}

/**
Expand Down Expand Up @@ -768,11 +773,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
public searchInput: ElementRef<HTMLInputElement> = null!;

/** @hidden @internal */
@ViewChild(IgxForOfDirective, { static: true })
public virtualScrollContainer!: IgxForOfDirective<any>;

@ViewChild(IgxForOfDirective, { read: IgxForOfDirective, static: true })
protected virtDir!: IgxForOfDirective<any>;
@ViewChild('virtualScroll', { static: true })
public virtualScrollContainer!: IgxVirtualScrollComponent<any>;

@ViewChild('dropdownItemContainer', { static: true })
protected dropdownContainer: ElementRef = null!;
Expand Down Expand Up @@ -877,7 +879,7 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
* ```
*/
public get virtualizationState(): IForOfState {
return this.virtDir.state;
return this._virtualizationState;
}
/**
* Sets the current state of the virtualized data.
Expand All @@ -888,7 +890,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
* ```
*/
public set virtualizationState(state: IForOfState) {
this.virtDir.state = state;
this._virtualizationState = { ...state };
void this.virtualScrollContainer?.scrollToIndex(state.startIndex ?? 0);
}

/**
Expand All @@ -911,18 +914,30 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
* ```
*/
public get totalItemCount(): number {
return this.virtDir.totalItemCount;
return this._totalItemCount;
}
/**
* Sets total count of the virtual data items, when using remote service.
*
* ```typescript
* // set
* this.combo.totalItemCount(remoteService.count);
* this.combo.totalItemCount = remoteService.count;
* ```
*/
public set totalItemCount(count: number) {
this.virtDir.totalItemCount = count;
if (this._totalItemCount === count) {
return;
}
this._totalItemCount = count;
this.cdr.markForCheck();

// Move an out-of-range viewport without relocating its loaded records.
// The record-window pipe excludes records past the new total.
const lastStart = Math.max(0, count - (this._virtualizationState.chunkSize ?? 0));
if ((this._virtualizationState.startIndex ?? 0) > lastStart) {
this._virtualizationState = { ...this._virtualizationState, startIndex: lastStart };
void this.virtualScrollContainer?.scrollToIndex(lastStart);
}
}

/** @hidden @internal */
Expand Down Expand Up @@ -968,8 +983,13 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
this._filteringOptions = value;
}

protected containerSize: number | undefined = undefined;
protected itemSize = undefined;
protected itemSize: number | undefined = undefined;

/** The wanted window, in the shape `virtualizationState` and `dataPreLoad` use. */
private _virtualizationState: IForOfState = { startIndex: 0, chunkSize: 0 };
/** Where the records currently bound sit, which a pending request has not moved yet. */
private _loadedStartIndex = 0;
private _totalItemCount = 0;
protected _data: any[] = [];
protected _value: any[] = [];
protected _displayValue = '';
Expand Down Expand Up @@ -1063,22 +1083,55 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
this.manageRequiredAsterisk();
this.cdr.detectChanges();
}
this.virtDir.chunkPreload.pipe(takeUntil(this.destroy$)).subscribe((e: IForOfState) => {
const eventArgs: IForOfState = Object.assign({}, e, { owner: this });
this.dataPreLoad.emit(eventArgs);
});
this.dropdown?.opening.subscribe((_args: IBaseCancelableBrowserEventArgs) => {
// calculate the container size and item size based on the sizes from the DOM
const dropdownContainerHeight = this.dropdownContainer.nativeElement.getBoundingClientRect().height;
if (dropdownContainerHeight) {
this.containerSize = parseFloat(dropdownContainerHeight);
}
// Take the row height from a real item, for the combos that do not set itemHeight.
if (this.dropdown.children?.first) {
this.itemSize = this.dropdown.children.first.element.nativeElement.getBoundingClientRect().height;
}
});
}

/** @hidden @internal The height the list gets, for the pass that opens the drop-down. */
protected get viewportSize(): number {
return this.itemsMaxHeight || this.estimatedItemSize * this.itemsInContainer;
}

/** @hidden @internal The size rows are assumed to be until they are measured. */
protected get estimatedItemSize(): number {
return this.itemHeight || this.itemSize || DEFAULT_ITEM_SIZE;
}

/** @hidden @internal Where the loaded items sit in the collection they came from. */
protected get virtualStartIndex(): number {
return this._loadedStartIndex;
}

/**
* @hidden @internal
* Reports the wanted window as `virtualizationState` and asks for the data behind it.
*/
public handleVirtualStateChange(state: VirtualScrollState): void {
const chunkSize = state.endIndex - state.startIndex + 1;
if (this._virtualizationState.startIndex === state.startIndex &&
this._virtualizationState.chunkSize === chunkSize) {
return;
Comment on lines +1113 to +1117
}

const initial = !this._virtualizationState.chunkSize;
const startIndex = state.startIndex;
this._virtualizationState = { startIndex, chunkSize };

// The first window a list reports can already be covered by the page it was given,
// and then there is nothing to fetch. Later windows are always reported, so a reply
// to a range the list has left is superseded rather than left in flight.
if (initial && startIndex >= this._loadedStartIndex &&
state.endIndex <= this._loadedStartIndex + this._data.length - 1) {
return;
}

this.dataPreLoad.emit({ ...this._virtualizationState, owner: this });
}

/** @hidden @internal */
public ngOnDestroy(): void {
this.destroy$.next();
Expand Down Expand Up @@ -1202,7 +1255,7 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
this.customValueFlag = false;
this.searchInput?.nativeElement.focus();
this.dropdown.focusedItem = null;
this.virtDir.scrollTo(0);
void this.virtualScrollContainer?.scrollToIndex(0);
}

/** @hidden @internal */
Expand All @@ -1219,14 +1272,35 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
owner: this,
cancel: false
};
const restore = this.resetVirtualizationState();
this.searchInputUpdate.emit(args);

if (args.cancel) {
this.filterValue = null!;
restore();
} else {
void this.virtualScrollContainer?.scrollToIndex(0);
}
}
this.checkMatch();
}

/**
* @hidden @internal
* Reports the start of the list without moving it. Returns a callback that puts it back.
*/
private resetVirtualizationState(): () => void {
const previous = this._virtualizationState;
if (previous.startIndex === 0) {
return () => { };
}

this._virtualizationState = { startIndex: 0, chunkSize: previous.chunkSize };
return () => {
this._virtualizationState = previous;
};
}

/**
* Event handlers
*
Expand Down
Loading