Enhanced return details and search components with new features and improvements.

-  **Feature**: Added InViewport directive for element visibility detection
-  **Feature**: Introduced new button for navigation in return details
- 🛠️ **Refactor**: Improved scroll position restoration logic and removed deprecated files
- 📚 **Docs**: Updated README with usage instructions for new directives

Ref: #5034
This commit is contained in:
Lorenz Hilpert
2025-04-08 10:51:24 +02:00
parent 62d0783e88
commit 8ca7977f7c
17 changed files with 287 additions and 148 deletions

View File

@@ -1,7 +1,66 @@
# utils-scroll-position
# Scroll Position Library
This library was generated with [Nx](https://nx.dev).
## Overview
## Running unit tests
Provides utilities for storing, restoring, and observing scroll position across navigation or component view changes.
Run `nx test utils-scroll-position` to execute the unit tests.
## Features
- Store current scroll position in session storage.
- Restore saved scroll position with an optional delay.
- Observe when an element enters or leaves the viewport.
## Usage
### Storing Scroll Position
Call the function to save the current scroll position:
```typescript
import { storeScrollPosition } from '@isa/utils/scroll-position';
storeScrollPosition();
```
### Restoring Scroll Position
Inject the restore function and call it with an optional delay:
```typescript
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
const restorePosition = injectRestoreScrollPosition();
await restorePosition(200);
```
### Automatic Restoration
Provide environment initializer in your app module to auto-save scroll positions:
```typescript
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
provideScrollPositionRestoration();
```
### Marking a Route for Auto Restoration
To enable automatic scroll restoration on a specific route, add the “scrollPositionRestoration” property in its data:
```typescript
{
path: 'example',
component: ExampleComponent,
data: {
scrollPositionRestoration: true
}
},
```
### Detecting Element Visibility
Apply the directive to a component template to emit true or false when entering or exiting the viewport:
```html
<div utilScrolledIntoViewport (utilScrolledIntoViewport)="onVisibilityChange($event)">...</div>
```

View File

@@ -1,2 +1,3 @@
export * from './lib/scroll-position-restoration';
export * from './lib/scrolled-into-viewport.directive';
export * from './lib/inject-restore-scroll-position';
export * from './lib/provide-scroll-position-restoration';
export * from './lib/store-scroll-position';

View File

@@ -0,0 +1,29 @@
import { ViewportScroller } from '@angular/common';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { SessionStorageProvider } from '@isa/core/storage';
/**
* Returns a function that restores the scroll position from session storage.
*
* The returned function waits for an optional delay to ensure the DOM is ready before scrolling.
*
* @returns A function accepting an optional delay (in milliseconds) and restoring scroll position.
*/
export function injectRestoreScrollPosition(): () => Promise<void> {
const router = inject(Router);
const viewportScroller = inject(ViewportScroller);
const sessionStorage = inject(SessionStorageProvider);
return async (delay = 0) => {
const url = router.url;
const position = await sessionStorage.get(url);
if (position) {
// wait for the next tick to ensure the DOM is ready
await new Promise((r) => setTimeout(r, delay));
sessionStorage.clear(url);
viewportScroller.scrollToPosition(position as [number, number]);
}
};
}

View File

@@ -0,0 +1,42 @@
import { ViewportScroller } from '@angular/common';
import { EnvironmentProviders, inject, provideEnvironmentInitializer } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { SessionStorageProvider } from '@isa/core/storage';
import { getDeepestActivatedRoute } from './utils/get-deepest-activated-route';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
/**
* Provides an environment initializer that restores scroll position upon navigation and page unload.
*
* Listens to router navigation events and the window's beforeunload event to store the current scroll position.
*
* @returns EnvironmentProviders for scroll position restoration.
*/
export function provideScrollPositionRestoration(): EnvironmentProviders {
return provideEnvironmentInitializer(() => {
const router = inject(Router);
const viewportScroller = inject(ViewportScroller);
const sessionStorage = inject(SessionStorageProvider);
function storeScrollPosition() {
const url = router.url;
const route = getDeepestActivatedRoute(router.routerState.root);
if (route.snapshot.data?.['scrollPositionRestoration']) {
sessionStorage.set(url, viewportScroller.getScrollPosition());
}
}
if (window) {
window.addEventListener('beforeunload', () => {
storeScrollPosition();
});
}
router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
if (event instanceof NavigationStart) {
storeScrollPosition();
}
});
});
}

View File

@@ -1,75 +0,0 @@
import { ViewportScroller } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EnvironmentProviders, inject, provideEnvironmentInitializer } from '@angular/core';
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
import { SessionStorageProvider } from '@isa/core/storage';
// const route: Route = {
// component: AnyComponent,
// data: {
// scrollPositionRestoration: true
// }
// }
const getDeepestActivatedRoute = (route: ActivatedRoute): ActivatedRoute => {
while (route.firstChild) {
route = route.firstChild;
}
return route;
};
export function provideScrollPositionRestoration(): EnvironmentProviders {
return provideEnvironmentInitializer(() => {
const router = inject(Router);
const viewportScroller = inject(ViewportScroller);
const sessionStorage = inject(SessionStorageProvider);
function storeScrollPosition() {
const url = router.url;
const route = getDeepestActivatedRoute(router.routerState.root);
if (route.snapshot.data?.['scrollPositionRestoration']) {
sessionStorage.set(url, viewportScroller.getScrollPosition());
}
}
if (window) {
window.addEventListener('beforeunload', () => {
storeScrollPosition();
});
}
router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
if (event instanceof NavigationStart) {
storeScrollPosition();
}
});
});
}
export async function storeScrollPosition() {
const router = inject(Router);
const viewportScroller = inject(ViewportScroller);
const sessionStorage = inject(SessionStorageProvider);
const url = router.url;
sessionStorage.set(url, viewportScroller.getScrollPosition());
}
export function injectRestoreScrollPosition(): () => Promise<void> {
const router = inject(Router);
const viewportScroller = inject(ViewportScroller);
const sessionStorage = inject(SessionStorageProvider);
return async (delay = 0) => {
const url = router.url;
const position = await sessionStorage.get(url);
if (position) {
// wait for the next tick to ensure the DOM is ready
await new Promise((r) => setTimeout(r, delay));
sessionStorage.clear(url);
viewportScroller.scrollToPosition(position as [number, number]);
}
};
}

View File

@@ -1,38 +0,0 @@
import { Directive, ElementRef, AfterViewInit, OnDestroy, output } from '@angular/core';
@Directive({
selector: '[utilScrolledIntoViewport]',
})
export class ScrolledIntoViewportDirective implements AfterViewInit, OnDestroy {
/**
* Emits true when the element enters the viewport and false when it leaves.
*/
scrolledIntoViewport = output<boolean>({ alias: 'utilScrolledIntoViewport' });
private observer: IntersectionObserver | null = null;
constructor(private elementRef: ElementRef<HTMLElement>) {}
ngAfterViewInit(): void {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.scrolledIntoViewport.emit(true);
} else {
this.scrolledIntoViewport.emit(false);
}
});
},
{ threshold: [0, 0.1, 0.5, 1.0] },
);
this.observer.observe(this.elementRef.nativeElement);
}
ngOnDestroy(): void {
if (this.observer) {
this.observer.disconnect();
}
}
}

View File

@@ -0,0 +1,17 @@
import { ViewportScroller } from '@angular/common';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { SessionStorageProvider } from '@isa/core/storage';
/**
* Stores the current scroll position in session storage.
* Uses the current router URL as the key.
*/
export async function storeScrollPosition() {
const router = inject(Router);
const viewportScroller = inject(ViewportScroller);
const sessionStorage = inject(SessionStorageProvider);
const url = router.url;
sessionStorage.set(url, viewportScroller.getScrollPosition());
}

View File

@@ -0,0 +1,14 @@
import { ActivatedRoute } from '@angular/router';
/**
* Recursively retrieves the deepest activated route.
*
* @param route - The starting ActivatedRoute instance.
* @returns The deepest ActivatedRoute found.
*/
export const getDeepestActivatedRoute = (route: ActivatedRoute): ActivatedRoute => {
while (route.firstChild) {
route = route.firstChild;
}
return route;
};