diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts index 30d1cf9796..0bf15dff88 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts @@ -1,8 +1,10 @@ import { + afterNextRender, AfterViewInit, ChangeDetectionStrategy, Component, computed, + DestroyRef, inject, Injector, LOCALE_ID, @@ -17,9 +19,10 @@ import { contentChild, viewChild, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; -import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; +import { Observable, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { @@ -92,6 +95,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; }) export class ExtensibleTableComponent implements AfterViewInit, OnDestroy { readonly #injector = inject(Injector); + readonly #destroyRef = inject(DestroyRef); readonly getInjected = this.#injector.get.bind(this.#injector); protected readonly locale = inject(LOCALE_ID); protected readonly config = inject(ConfigStateService); @@ -186,6 +190,10 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr }, ); + private horizontalScrollOffset = 0; + + private hasPendingHorizontalScrollOffset = false; + hasAtLeastOnePermittedAction: boolean; readonly propList: EntityPropList; @@ -232,6 +240,7 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr } this._data.set(dataValue.map((record, index) => this.prepareRecord(record, index))); + this.restoreHorizontalScrollOffset(); }); } @@ -363,13 +372,66 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr ngAfterViewInit(): void { if (!this.infiniteScroll()) { this.list() - ?.requestStatus$?.pipe(filter(status => status === 'loading')) - .subscribe(() => { - this._data.set([]); + ?.requestStatus$?.pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe(status => { + if (status === 'loading') { + this.rememberHorizontalScrollOffset(); + this._data.set([]); + return; + } + + // A failed request never reaches the data input, restore from here instead + if (status === 'error') { + this.restoreHorizontalScrollOffset(); + } }); } } + private getBodyElement(): HTMLElement | null { + if (!this.isBrowser) { + return null; + } + + return this.table()?.element?.querySelector('datatable-body') ?? null; + } + + // The element carrying the column width is gone while loading without rows, so the browser + // resets the horizontal scroll position and the header keeps the offset it had before + private rememberHorizontalScrollOffset(): void { + const body = this.getBodyElement(); + this.hasPendingHorizontalScrollOffset = true; + + // Not scrollable while a previous request is still in flight, keep the offset taken back then + if (body && body.scrollWidth > body.clientWidth) { + this.horizontalScrollOffset = body.scrollLeft; + } + } + + private restoreHorizontalScrollOffset(): void { + if (!this.hasPendingHorizontalScrollOffset) { + return; + } + + this.hasPendingHorizontalScrollOffset = false; + + afterNextRender( + () => { + const body = this.getBodyElement(); + + if (!body) { + return; + } + + body.scrollLeft = this.horizontalScrollOffset; + + // The header only follows a scroll event, scrolling to the same position does not raise one + body.dispatchEvent(new Event('scroll')); + }, + { injector: this.#injector }, + ); + } + ngOnDestroy(): void { this.loadMoreSubscription.unsubscribe(); }