Files
Portfolio/src/app/shared/motion/reveal.directive.ts

78 lines
2.1 KiB
TypeScript

import { DOCUMENT } from '@angular/common';
import {
afterNextRender,
DestroyRef,
Directive,
ElementRef,
inject,
Injector,
input,
} from '@angular/core';
import { isBrowserPlatform, prefersReducedMotion } from '../../core/platform/browser';
@Directive({
selector: '[appReveal]',
})
export class RevealDirective {
readonly appRevealThreshold = input(0.12);
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
private readonly document = inject(DOCUMENT);
private readonly injector = inject(Injector);
private readonly destroyRef = inject(DestroyRef);
private readonly isBrowser = isBrowserPlatform();
private readonly reducedMotion = prefersReducedMotion();
private observer: IntersectionObserver | null = null;
constructor() {
if (!this.isBrowser || this.reducedMotion || !this.canObserve()) {
this.revealNow();
return;
}
afterNextRender(() => this.observe(), { injector: this.injector });
this.destroyRef.onDestroy(() => this.disconnect());
}
private canObserve(): boolean {
const view = this.document.defaultView;
return !!view && typeof view.IntersectionObserver === 'function';
}
private observe(): void {
const view = this.document.defaultView;
if (!view || typeof view.IntersectionObserver !== 'function') {
this.revealNow();
return;
}
this.observer = new view.IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
this.host.nativeElement.classList.remove('reveal-pending');
this.host.nativeElement.classList.add('is-revealed');
this.disconnect();
return;
}
this.host.nativeElement.classList.add('reveal-pending');
},
{
rootMargin: '0px 0px -8% 0px',
threshold: this.appRevealThreshold(),
},
);
this.observer.observe(this.host.nativeElement);
}
private revealNow(): void {
this.host.nativeElement.classList.add('is-revealed');
}
private disconnect(): void {
this.observer?.disconnect();
this.observer = null;
}
}