typeof HTMLCanvasElement.prototype.getContext
>jest.fn()
+if (!HTMLElement.prototype.scrollTo) {
+ HTMLElement.prototype.scrollTo = jest.fn()
+}
+
jest.mock('uuid', () => ({
v4: jest.fn(() =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char: string) => {
}
</ul>
-<div [ngbNavOutlet]="nav" class="mt-2"></div>
-
-<cdk-virtual-scroll-viewport
- itemSize="20"
- class="bg-dark p-3 text-light font-monospace log-container"
- #logContainer>
+<div #logContainer class="bg-dark text-light font-monospace log-container p-3" (scroll)="onScroll()">
@if (loading && !logFiles.length) {
<div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
+ } @else {
+ <p *ngFor="let log of logs" class="m-0 p-0" [ngClass]="'log-entry-' + log.level">{{log.message}}</p>
}
- <p *cdkVirtualFor="let log of logs"
- class="m-0 p-0"
- [ngClass]="'log-entry-' + log.level">
- {{log.message}}
- </p>
-</cdk-virtual-scroll-viewport>
+</div>
+<button
+ type="button"
+ class="btn btn-sm btn-secondary jump-to-bottom position-fixed bottom-0 end-0 m-5"
+ [class.visible]="showJumpToBottom"
+ (click)="scrollToBottom()"
+>
+ ↓ <span i18n>Jump to bottom</span>
+</button>
jest.advanceTimersByTime(1)
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1)
})
+
+ it('should update jump to bottom visibility on scroll', () => {
+ component.showJumpToBottom = false
+ jest.spyOn(component as any, 'isNearBottom').mockReturnValue(false)
+ component.onScroll()
+ expect(component.showJumpToBottom).toBe(true)
+ })
})
-import {
- CdkVirtualScrollViewport,
- ScrollingModule,
-} from '@angular/cdk/scrolling'
import { CommonModule } from '@angular/common'
import {
ChangeDetectorRef,
Component,
+ ElementRef,
OnDestroy,
OnInit,
ViewChild,
CommonModule,
FormsModule,
ReactiveFormsModule,
- CdkVirtualScrollViewport,
- ScrollingModule,
],
})
export class LogsComponent
public limit: number = 5000
+ public showJumpToBottom = false
+
private readonly limitChange$ = new Subject<number>()
- @ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
+ @ViewChild('logContainer') logContainer: ElementRef<HTMLElement>
ngOnInit(): void {
this.limitChange$
reloadLogs() {
this.loading = true
+ const shouldStickToBottom = this.isNearBottom()
this.logService
.get(this.activeLog, this.limit)
.pipe(takeUntil(this.unsubscribeNotifier))
})
if (hasChanges) {
this.logs = parsed
- this.scrollToBottom()
+ if (shouldStickToBottom) {
+ this.scrollToBottom()
+ }
+ this.showJumpToBottom = !shouldStickToBottom
}
},
error: () => {
}
scrollToBottom(): void {
- this.changedetectorRef.detectChanges()
- if (this.logContainer) {
- this.logContainer.scrollToIndex(this.logs.length - 1)
+ const viewport = this.logContainer?.nativeElement
+ if (!viewport) {
+ return
}
+ this.changedetectorRef.detectChanges()
+ viewport.scrollTop = viewport.scrollHeight
+ this.showJumpToBottom = false
+ }
+
+ private isNearBottom(): boolean {
+ if (!this.logContainer?.nativeElement) return true
+ const distanceFromBottom =
+ this.logContainer.nativeElement.scrollHeight -
+ this.logContainer.nativeElement.scrollTop -
+ this.logContainer.nativeElement.clientHeight
+ return distanceFromBottom <= 40
+ }
+
+ onScroll(): void {
+ this.showJumpToBottom = !this.isNearBottom()
}
}