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
10 changes: 10 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 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
92 changes: 70 additions & 22 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 @@ -768,11 +772,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 +878,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 +889,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,7 +913,7 @@ 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.
Expand All @@ -922,7 +924,7 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
* ```
*/
public set totalItemCount(count: number) {
this.virtDir.totalItemCount = count;
this._totalItemCount = count;
}

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

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

/** The window the list renders, in the shape `virtualizationState` and `dataPreLoad` use. */
private _virtualizationState: IForOfState = { startIndex: 0, chunkSize: 0 };
private _totalItemCount = 0;
protected _data: any[] = [];
protected _value: any[] = [];
protected _displayValue = '';
Expand Down Expand Up @@ -1063,22 +1068,44 @@ 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._virtualizationState.startIndex ?? 0;
}

/**
* @hidden @internal
* Reports the rendered 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 +1098 to +1102
}

this._virtualizationState = { startIndex: state.startIndex, chunkSize };
this.dataPreLoad.emit({ ...this._virtualizationState, owner: this });
Comment on lines +1105 to +1106
}

/** @hidden @internal */
public ngOnDestroy(): void {
this.destroy$.next();
Expand Down Expand Up @@ -1202,7 +1229,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 +1246,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
47 changes: 28 additions & 19 deletions projects/igniteui-angular/combo/src/combo/combo.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,28 +70,37 @@
}
<ng-container *ngTemplateOutlet="headerTemplate">
</ng-container>
@let itemWindow = data!
| comboFiltering:filterValue:displayKey:filteringOptions:filterFunction:disableFiltering
| comboGrouping:groupKey:valueKey:groupSortingDirection:compareCollator
| comboDataWindow:totalItemCount:virtualStartIndex;
<div #dropdownItemContainer class="igx-combo__content" [style.overflow]="'hidden'"
[style.maxHeight.rem]="itemsMaxHeightInRem" [igxDropDownItemNavigation]="dropdown" (focus)="dropdown.onFocus()"
[tabindex]="dropdown.collapsed ? -1 : 0" [attr.id]="dropdown.id" aria-multiselectable="true"
[attr.aria-activedescendant]="activeDescendant">
<igx-combo-item [itemHeight]="itemHeight" *igxFor="let item of data!
| comboFiltering:filterValue:displayKey:filteringOptions:filterFunction:disableFiltering
| comboGrouping:groupKey:valueKey:groupSortingDirection:compareCollator;
index as rowIndex; initialChunkSize: 10; containerSize: itemsMaxHeight || containerSize; itemSize: itemHeight || itemSize, scrollOrientation: 'vertical';"
[value]="item" [isHeader]="item?.isHeader" [index]="rowIndex" [role]="item?.isHeader? 'group' : 'option'">
@if (item?.isHeader) {
<ng-container
*ngTemplateOutlet="headerItemTemplate ? headerItemTemplate : headerItemBase;
context: {$implicit: item, data: data, valueKey: valueKey, groupKey: groupKey, displayKey: displayKey}">
</ng-container>
}
<!-- if item is 'null' it should be displayed and !!(item?.isHeader) would resolve it to 'false' and not display it -->
@if (!item?.isHeader) {
<ng-container #listItem
*ngTemplateOutlet="template; context: {$implicit: item, data: data, valueKey: valueKey, displayKey: displayKey};">
</ng-container>
}
</igx-combo-item>
<igx-virtual-scroll #virtualScroll
[dataWindow]="itemWindow"
[estimatedItemSize]="estimatedItemSize"
[initialViewportSize]="viewportSize"
(stateChange)="handleVirtualStateChange($event)">
Comment on lines +81 to +85
<ng-template igxVirtualItem let-item let-rowIndex="index">
<igx-combo-item [itemHeight]="itemHeight"
[value]="item" [isHeader]="item?.isHeader" [index]="rowIndex" [role]="item?.isHeader? 'group' : 'option'">
@if (item?.isHeader) {
<ng-container
*ngTemplateOutlet="headerItemTemplate ? headerItemTemplate : headerItemBase;
context: {$implicit: item, data: data, valueKey: valueKey, groupKey: groupKey, displayKey: displayKey}">
</ng-container>
}
<!-- if item is 'null' it should be displayed and !!(item?.isHeader) would resolve it to 'false' and not display it -->
@if (!item?.isHeader) {
<ng-container #listItem
*ngTemplateOutlet="template; context: {$implicit: item, data: data, valueKey: valueKey, displayKey: displayKey};">
</ng-container>
}
</igx-combo-item>
</ng-template>
</igx-virtual-scroll>
</div>

@if (filteredData?.length === 0 || isAddButtonVisible()) {
Expand All @@ -105,7 +114,7 @@
@if (isAddButtonVisible()) {
<igx-combo-add-item [itemHeight]="itemHeight"
[tabindex]="dropdown.collapsed ? -1 : customValueFlag ? 1 : -1" class="igx-combo__add-item" role="button"
[attr.aria-label]="resourceStrings.igx_combo_addCustomValues_placeholder" [index]="virtualScrollContainer.igxForOf!.length">
[attr.aria-label]="resourceStrings.igx_combo_addCustomValues_placeholder" [index]="itemWindow.totalCount">
<ng-container *ngTemplateOutlet="addItemTemplate ? addItemTemplate : addItemDefault">
</ng-container>
</igx-combo-add-item>
Expand Down
Loading