Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ All notable changes for each version of this project will be documented in this
- The `outlet` input is now applied to the dropdown/dialog.
- The dialog header no longer throws a missing locale data error for a `locale` without registered Angular locale data when `provideIgniteIntl()` is used.
- The `aria-valuemin` and `aria-valuemax` of the selected seconds item no longer depend on the milliseconds of the value.
- `IgxForOfDirective`
- Growing the container of a virtualized list or grid scrolled to its end (for example by resizing the grid) no longer renders the rows or columns out of order or shifts the visible content by one item (#17575).
- In right-to-left direction, changing the items of a horizontally scrolled `igxFor` no longer moves the visible items away from the scroll position.

## 22.2.4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,39 @@ describe('IgxForOf directive -', () => {
expect(parseInt(displayContainer.style.left, 10)).toEqual(0);
});

for (const dir of ['ltr', 'rtl']) {
it(`should keep the horizontal scroll offset if igxForOf changes while scrolled (${dir})`, () => {
const sign = dir === 'rtl' ? -1 : 1;
fix.debugElement.nativeElement.dir = dir;
fix.componentInstance.width = '500px';
fix.componentInstance.cols = [
{ field: '1', width: 100 },
{ field: '2', width: 200 },
{ field: '3', width: 200 },
{ field: '4', width: 200 },
{ field: '5', width: 300 }
];
fix.detectChanges();

fix.componentInstance.scrollLeft(sign * 250);
fix.detectChanges();
const firstDir = fix.componentInstance.childVirtDirs.first;
expect(firstDir.state.startIndex).toBe(1);
expect(parseFloat(displayContainer.style.left)).toBe(-sign * 150);

// widening the last column keeps the scroll position and the start index
fix.componentInstance.cols = [
...fix.componentInstance.cols.slice(0, 4),
{ field: '5', width: 400 }
];
fix.detectChanges();

expect(Math.abs(horizontalScroller.scrollLeft)).toBe(250);
expect(firstDir.state.startIndex).toBe(1);
expect(parseFloat(displayContainer.style.left)).toBe(-sign * 150);
});
}

it('should allow scroll in rtl direction.', () => {
fix.debugElement.nativeElement.dir = 'rtl';
fix.detectChanges();
Expand All @@ -183,6 +216,40 @@ describe('IgxForOf directive -', () => {
expect(state.startIndex).toBe(1);
});

for (const dir of ['ltr', 'rtl']) {
it(`should keep the scrolled content in place when growing the container prepends items (${dir})`, () => {
const sign = dir === 'rtl' ? -1 : 1;
fix.debugElement.nativeElement.dir = dir;
fix.componentInstance.width = '200px';
fix.componentInstance.cols = [
{ field: '1', width: 100 },
{ field: '2', width: 100 },
{ field: '3', width: 100 },
{ field: '4', width: 100 },
{ field: '5', width: 100 }
];
fix.detectChanges();

fix.componentInstance.scrollLeft(sign * 250);
fix.detectChanges();
const firstDir = fix.componentInstance.childVirtDirs.first;
expect(firstDir.state.startIndex).toBe(2);
expect(firstDir.state.chunkSize).toBe(3);
expect(parseFloat(displayContainer.style.left)).toBe(-sign * 50);

// growing by 1px adds one item but leaves the scroll position unclamped,
// so no scroll event follows to restore the offset
fix.componentInstance.width = '201px';
fix.detectChanges();

expect(Math.abs(horizontalScroller.scrollLeft)).toBe(250);
expect(firstDir.state.startIndex).toBe(1);
expect(firstDir.state.chunkSize).toBe(4);
expect(parseFloat(displayContainer.style.left)).toBe(-sign * 150);
expect(displayContainer.children[0].textContent).toBe(fix.componentInstance.data[0]['2'].toString());
});
}

it('should display the correct chunk items on resizing the container', async () => {
// initially the container's width is narrow enough to be scrollable
fix.componentInstance.width = '200px';
Expand Down Expand Up @@ -627,6 +694,29 @@ describe('IgxForOf directive -', () => {
}
});

it('should keep rows in order when the viewport grows at the bottom', async () => {
Comment thread
mddragnev marked this conversation as resolved.
Resolved
const component = fix.componentInstance;
// scroll 25px short of the end, so that growing by 10px does not clamp the scroll position
const scrollTop = 50000 * 50 - 400 - 25;
component.scrollTop(scrollTop);
fix.detectChanges();
await wait();

component.height = '410px';
fix.detectChanges();
await wait();

const rows = Array.from(displayContainer.children);
const startIndex = component.data.length - rows.length;
rows.forEach((row, index) => {
const secondCell = row.querySelector(DISPLAY_CONTAINER)?.children[1];
expect(secondCell?.textContent)
.toBe(component.data[startIndex + index]['1'].toString());
});
// the rows are offset so that the content at the scroll position stays in view
expect(displayContainer.style.transform).toBe(`translateY(${startIndex * 50 - scrollTop}px)`);
});

it('should scroll to wheel event correctly', async () => {
fix.componentInstance.parentVirtDir.dc.instance._scrollInertia.smoothingDuration = 0;
/* 120 is default mousewheel on Chrome, scroll 2 records down */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1486,8 +1486,14 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U

protected _recalcOnContainerChange(containerSizeInfo: { prevSize: number; newSize: number } | null = null) {
const prevChunkSize = this.state.chunkSize;
const prevStartIndex = this.state.startIndex!;
this.applyChunkSizeChange();
this._recalcScrollBarSize(containerSizeInfo);
if (prevStartIndex !== this.state.startIndex) {
// items were prepended to fill the grown container, so sync the display container offset
// to keep the scroll position's content in view
this._updateScrollOffset();
}
if (prevChunkSize !== this.state.chunkSize) {
this.chunkLoad.emit(this.state);
}
Expand Down Expand Up @@ -1523,24 +1529,53 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
* If there exists an element that we can create embedded view for creates it, appends it and updates chunkSize
*/
protected addLastElem() {
let elemIndex = this.state.startIndex! + this.state.chunkSize!;
if (this.insertChunkElem()) {
this._zone.run(() => this.cdr.markForCheck());
}
}

/**
* Creates an embedded view for the item after the current chunk and updates chunkSize.
* When the chunk already reaches the end of the data, the view is created for the item before it
* and prepended instead, so that the rendered items stay in order.
*/
protected insertChunkElem(): EmbeddedViewRef<any> | null {
if (!this.isRemote && !this.igxForOf) {
return;
return null;
}

let elemIndex = this.state.startIndex! + this.state.chunkSize!;
let prepend = false;
if (elemIndex >= this.igxForOf!.length) {
elemIndex = this.igxForOf!.length - this.state.chunkSize!;
if (!this.isRemote && this.state.startIndex! > 0) {
elemIndex = --this.state.startIndex!;
prepend = true;
Comment on lines +1551 to +1552

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.

@teodosiah ,enabled auto-merge but it won't happen until this comment is resolved one way or another. If you think it's wrong, you can resolve it with an explanation.

} else {
elemIndex = this.igxForOf!.length - this.state.chunkSize!;
}
}
const input = this.igxForOf![elemIndex];
const embeddedView = this.dc.instance._vcr.createEmbeddedView(
this._template,
new IgxForOfContext<T, U>(input, this.igxForOf!, this.getContextIndex(input), this.igxForOf!.length) as any
this.createTemplateContext(input) as any,
{ index: prepend ? 0 : undefined }
);

this._embeddedViews.push(embeddedView);
if (prepend) {
this._embeddedViews.unshift(embeddedView);
} else {
this._embeddedViews.push(embeddedView);
}
this.state.chunkSize!++;
return embeddedView;
}

this._zone.run(() => this.cdr.markForCheck());
/**
* @hidden
* Creates the template context for an item rendered by insertChunkElem().
*/
protected createTemplateContext(input: T): IgxForOfContext<T, U> {
return new IgxForOfContext<T, U>(input, this.igxForOf!, this.getContextIndex(input), this.igxForOf!.length);
}

/**
Expand Down Expand Up @@ -1580,9 +1615,9 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U

protected _updateScrollOffset() {
let scrollOffset = 0;
const currentScroll = this.scrollPosition;
const currentScroll = Math.abs(this.scrollPosition);
if (this._virtRatio !== 1) {
this._calcVirtualScrollPosition(this.scrollPosition);
this._calcVirtualScrollPosition(currentScroll);
scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
} else {
const scroll = this.scrollComponent.nativeElement;
Expand All @@ -1591,7 +1626,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
}
const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'transform';
this.dc.instance._viewContainer.element.nativeElement.style[dir] = this.igxForScrollOrientation === 'horizontal' ?
-(scrollOffset) + 'px' :
(this.isRTL ? scrollOffset : -scrollOffset) + 'px' :
`translateY(${-scrollOffset}px)`;
}

Expand Down Expand Up @@ -1968,23 +2003,14 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
}

protected override addLastElem() {
let elemIndex = this.state.startIndex! + this.state.chunkSize!;
if (!this.isRemote && !this.igxForOf) {
return;
const embeddedView = this.insertChunkElem();
if (embeddedView) {
this.subscribeToViewObserver(this.getViewObservedNode(embeddedView));
}
}

if (elemIndex >= this.igxForOf!.length) {
elemIndex = this.igxForOf!.length - this.state.chunkSize!;
}
const input = this.igxForOf![elemIndex];
const embeddedView = this.dc.instance._vcr.createEmbeddedView(
this._template,
new IgxGridForOfContext<T, U>(input, this.igxForOf!, this.getContextIndex(input), this.igxForOf!.length) as any
);

this._embeddedViews.push(embeddedView);
this.subscribeToViewObserver(this.getViewObservedNode(embeddedView));
this.state.chunkSize!++;
protected override createTemplateContext(input: T): IgxGridForOfContext<T, U> {
return new IgxGridForOfContext<T, U>(input, this.igxForOf!, this.getContextIndex(input), this.igxForOf!.length);
}

protected _updateViews(prevChunkSize: number | undefined) {
Expand Down
33 changes: 33 additions & 0 deletions projects/igniteui-angular/grids/grid/src/grid.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,39 @@ describe('IgxGrid Component Tests #grid', () => {
expect(grid.columns[1].field).toBe('index');
});

it('should preserve row ordering when the viewport grows at the bottom', async () => {
const fix = TestBed.createComponent(IgxGridTestComponent);
fix.componentInstance.data = Array.from({ length: 1000 }, (_, index) => ({ index, value: index }));
fix.detectChanges();
fix.detectChanges();

const grid = fix.componentInstance.grid;
const lastIndex = fix.componentInstance.data.length - 1;

grid.verticalScrollContainer.scrollTo(lastIndex);
await wait(100);

grid.height = '200px';
fix.detectChanges();
await wait(100);

grid.height = '600px';
fix.detectChanges();
await wait(100);

grid.verticalScrollContainer.scrollTo(lastIndex);
await wait(100);

const renderedIndexes = grid.rowList.toArray().map(row => row.data.index);
expect(renderedIndexes[renderedIndexes.length - 1]).toBe(lastIndex);
renderedIndexes.forEach((index, position) => {
if (position > 0) {
expect(index).toBe(renderedIndexes[position - 1] + 1);
}
});
fix.destroy();
});

it('should initialize grid with remote virtualization', async () => {
const fix = TestBed.createComponent(IgxGridRemoteVirtualizationComponent);
fix.detectChanges();
Expand Down
Loading