fix(isa-app-scroll-container): Fixed issue with reloading on several lists

Ref: #5237
This commit is contained in:
Nino
2025-12-22 15:00:57 +01:00
parent d9b653073b
commit ba09cb2508
3 changed files with 116 additions and 34 deletions

View File

@@ -11,7 +11,7 @@ import {
SimpleChanges,
} from '@angular/core';
import { Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { debounceTime, filter } from 'rxjs/operators';
@Directive({
selector: '[uiScrollContainer]',
@@ -27,30 +27,48 @@ export class UiScrollContainerDirective implements OnChanges, OnInit {
@Input()
deltaEnd = 0;
private scrollEvent$ = new Subject<Event>();
private scrollEvent$ = new Subject<void>();
/**
* Tracks the last scrollHeight when reachEnd was emitted.
* This prevents duplicate emissions at the same scroll position after content loads.
*/
private lastEmittedScrollHeight = 0;
@Output()
reachStart = this.scrollEvent$.pipe(
filter((event) => {
debounceTime(100),
filter(() => {
if (this.direction === 'vertical') {
const top = this.nativeElement.scrollTop;
return top <= this.deltaStart;
} else {
throw new Error('not implemented');
return this.nativeElement.scrollTop <= this.deltaStart;
}
throw new Error('Horizontal scroll not implemented');
}),
);
@Output()
reachEnd = this.scrollEvent$.pipe(
filter((event) => {
debounceTime(100),
filter(() => {
if (this.direction === 'vertical') {
const top = this.nativeElement.scrollTop;
const height = this.nativeElement.scrollHeight - this.nativeElement.clientHeight - this.deltaEnd;
return top >= height;
} else {
throw new Error('not implemented');
const { scrollTop, scrollHeight, clientHeight } = this.nativeElement;
const threshold = scrollHeight - clientHeight - this.deltaEnd;
const isAtEnd = scrollTop >= threshold;
if (!isAtEnd) {
return false;
}
// Only emit if scrollHeight changed (new content loaded)
// This prevents re-emitting when user is still at end after a load
if (scrollHeight !== this.lastEmittedScrollHeight) {
this.lastEmittedScrollHeight = scrollHeight;
return true;
}
return false;
}
throw new Error('Horizontal scroll not implemented');
}),
);
@@ -71,17 +89,33 @@ export class UiScrollContainerDirective implements OnChanges, OnInit {
ngOnChanges({ direction }: SimpleChanges): void {
if (direction) {
if (this.direction === 'horizontal') {
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-x', 'auto');
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-y', 'auto');
this.renderer.setStyle(
this.elementRef.nativeElement,
'overflow-x',
'auto',
);
this.renderer.setStyle(
this.elementRef.nativeElement,
'overflow-y',
'auto',
);
} else {
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-y', 'auto');
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-x', 'hidden');
this.renderer.setStyle(
this.elementRef.nativeElement,
'overflow-y',
'auto',
);
this.renderer.setStyle(
this.elementRef.nativeElement,
'overflow-x',
'hidden',
);
}
}
}
@HostListener('scroll', ['$event'])
onScroll(event: Event) {
this.scrollEvent$.next(event);
@HostListener('scroll')
onScroll() {
this.scrollEvent$.next();
}
}

View File

@@ -8,20 +8,27 @@
(reachEnd)="reachedEnd()"
(reachStart)="reachedStart()"
[deltaEnd]="deltaEnd"
>
@if (!loading) {
>
@if (!loading || itemLength > 0) {
<ng-content></ng-content>
} @else {
@if (useLoadAnimation) {
}
@if (loading && useLoadAnimation) {
@if (itemLength === 0 || itemLength === undefined) {
<!-- Initial load: show multiple skeletons -->
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
@for (skeletons of createSkeletons(); track skeletons) {
@for (skeleton of createSkeletons(); track skeleton) {
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
}
} @else {
<ui-content-loader [loading]="loading"></ui-content-loader>
<!-- Load more: show single skeleton at the end -->
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
}
}
@if (loading && !useLoadAnimation) {
<ui-content-loader [loading]="loading"></ui-content-loader>
}
@if (showSpacer && !loading) {
<div class="spacer"></div>

View File

@@ -4,8 +4,10 @@ import {
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
@@ -16,7 +18,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class UiScrollContainerComponent implements OnInit {
export class UiScrollContainerComponent implements OnInit, OnChanges {
@ViewChild('scrollContainer', { read: ElementRef, static: true })
scrollContainer: ElementRef;
@@ -61,14 +63,49 @@ export class UiScrollContainerComponent implements OnInit {
}
}
createSkeletons() {
if (this.itemLength && this.itemLength !== 0) {
return Array.from(Array(this.itemLength - 1), (_, i) => i);
} else {
return [];
ngOnChanges(changes: SimpleChanges): void {
// When new items are loaded, adjust scroll position so user can scroll down again
if (changes['itemLength']) {
const prevLength = changes['itemLength'].previousValue ?? 0;
const newLength = changes['itemLength'].currentValue ?? 0;
// Only adjust if items were added (not on initial load or reset)
if (newLength > prevLength && prevLength > 0) {
this.adjustScrollPositionAfterLoad();
}
}
}
/**
* After new items are loaded, adjust scroll position so user is not at the very end.
* This allows them to scroll down again to trigger the next load.
*/
private adjustScrollPositionAfterLoad(): void {
const el = this.scrollContainer?.nativeElement;
if (!el) return;
// Wait for DOM to update with new items
setTimeout(() => {
const maxScroll = el.scrollHeight - el.clientHeight;
const currentScroll = el.scrollTop;
// Only adjust if we're at or very near the end
if (currentScroll >= maxScroll - this.deltaEnd - 20) {
// Move scroll position up by deltaEnd + buffer so user has room to scroll
const offset = this.deltaEnd + 100;
const targetScroll = Math.max(0, maxScroll - offset);
el.scrollTop = targetScroll;
}
}, 50);
}
createSkeletons(): number[] {
if (this.itemLength && this.itemLength !== 0) {
return Array.from({ length: this.itemLength - 1 }, (_, i) => i);
}
return [];
}
reachedEnd() {
this.reachEnd.emit();
}
@@ -79,7 +116,8 @@ export class UiScrollContainerComponent implements OnInit {
get scrollPersantage() {
const scrollHeight =
this.scrollContainer?.nativeElement?.scrollHeight - this.scrollContainer?.nativeElement?.clientHeight;
this.scrollContainer?.nativeElement?.scrollHeight -
this.scrollContainer?.nativeElement?.clientHeight;
if (scrollHeight === 0) {
return 0;
}
@@ -95,7 +133,10 @@ export class UiScrollContainerComponent implements OnInit {
scrollTo(top: number) {
setTimeout(() => {
this.scrollContainer?.nativeElement?.scrollTo({ top, behavior: 'smooth' });
this.scrollContainer?.nativeElement?.scrollTo({
top,
behavior: 'smooth',
});
}, 0);
}