Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {Injector, NgZone} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {OverlayRef, ScrollStrategy} from '@angular/cdk/overlay';
import {EvoCloseScrollStrategyParams} from '../interfaces/evo-close-scroll-strategy-params';
import {filter, first, tap} from 'rxjs/operators';
import {createScrollStream} from '../utils/create-scroll-stream';
import {Subscription} from 'rxjs';
import {ScrollPosition} from '../interfaces/scroll-position';

export class EvoCloseScrollStrategy implements ScrollStrategy {
private readonly document: Document;
private readonly ngZone: NgZone;

private overlayRef: OverlayRef | null = null;
private scrollSubscription: Subscription | null = null;

private initialScrollPosition: ScrollPosition | null = null;

constructor(private readonly injector: Injector, private readonly params?: EvoCloseScrollStrategyParams) {
this.document = this.injector.get(DOCUMENT);
this.ngZone = this.injector.get(NgZone);
}

attach(overlayRef: OverlayRef): void {
this.overlayRef = overlayRef;
}

detach(): void {
this.disable();
this.overlayRef = null;
}

disable(): void {
if (!this.scrollSubscription) {
return;
}

this.scrollSubscription.unsubscribe();
this.scrollSubscription = null;
}

enable(): void {
if (this.scrollSubscription || !this.overlayRef) {
return;
}

this.ngZone.runOutsideAngular(() => {
this.initialScrollPosition = this.getCurrentScrollPosition();

this.scrollSubscription = createScrollStream(this.document, this.overlayRef)
.pipe(
filter(() => this.checkThreshold()),
first(),
tap(() => this.detachOverlay()),
)
.subscribe();
});
}

private detachOverlay(): void {
if (!this.overlayRef?.hasAttached()) {
return;
}

this.disable();
this.ngZone.run(() => this.overlayRef.detach());
}

private checkThreshold(): boolean {
const threshold = this.params?.threshold ?? 0;
const triggerEl = this.params?.triggerRef?.nativeElement;

if (!triggerEl || !this.initialScrollPosition) {
return true;
}

const scrollPosition = this.getCurrentScrollPosition();

if (!scrollPosition) {
return false;
}

const distanceY = Math.abs(scrollPosition.vertical - this.initialScrollPosition.vertical);
const distanceX = Math.abs(scrollPosition.horizontal - this.initialScrollPosition.horizontal);

return Math.max(distanceX, distanceY) > threshold;
}

private getCurrentScrollPosition(): ScrollPosition | null {
const element = this.params?.triggerRef?.nativeElement as Element;

if (!element) {
return null;
}

const rect = element.getBoundingClientRect();

return {
vertical: rect.top,
horizontal: rect.left,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {Injector, NgZone} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {OverlayRef, ScrollStrategy} from '@angular/cdk/overlay';
import {Subscription} from 'rxjs';
import {tap} from 'rxjs/operators';
import {createScrollStream} from '../utils/create-scroll-stream';

export class EvoRepositionScrollStrategy implements ScrollStrategy {
private readonly document: Document;
private readonly ngZone: NgZone;

private overlayRef: OverlayRef | null = null;

private scrollSubscription: Subscription | null = null;

constructor(private readonly injector: Injector) {
this.document = this.injector.get(DOCUMENT);
this.ngZone = this.injector.get(NgZone);
}

attach(overlayRef: OverlayRef): void {
this.overlayRef = overlayRef;
}

detach(): void {
this.disable();
this.overlayRef = null;
}

disable(): void {
if (!this.scrollSubscription) {
return;
}

this.scrollSubscription.unsubscribe();
this.scrollSubscription = null;
}

enable(): void {
if (this.scrollSubscription || !this.overlayRef) {
return;
}

this.ngZone.runOutsideAngular(() => {
this.scrollSubscription = createScrollStream(this.document, this.overlayRef)
.pipe(
tap(() => {
if (this.isOverlayScrolledOutsideView()) {
this.detachOverlay();
return;
}

this.updatePosition();
}),
)
.subscribe();
});
}

private updatePosition(): void {
if (!this.overlayRef?.hasAttached()) {
return;
}

this.ngZone.run(() => this.overlayRef.updatePosition());
}

private detachOverlay(): void {
if (!this.overlayRef?.hasAttached()) {
return;
}

this.disable();
this.ngZone.run(() => this.overlayRef.detach());
}

private isOverlayScrolledOutsideView(): boolean {
if (!this.overlayRef?.hasAttached()) {
return true;
}

const overlayRect = this.overlayRef.overlayElement.getBoundingClientRect();
const pageHeight = this.document.documentElement.clientHeight;
const pageWidth = this.document.documentElement.clientWidth;

return (
overlayRect.bottom < 0 ||
overlayRect.top > pageHeight ||
overlayRect.right < 0 ||
overlayRect.left > pageWidth
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {NoopScrollStrategy, ScrollStrategy} from '@angular/cdk/overlay';
import {Injectable, Injector} from '@angular/core';
import {EvoCloseScrollStrategy} from './evo-close-scroll-strategy';
import {EvoRepositionScrollStrategy} from './evo-reposition-scroll-strategy';
import {EvoCloseScrollStrategyParams} from '../interfaces/evo-close-scroll-strategy-params';

@Injectable()
export class EvoScrollStrategyOptions {
constructor(private readonly injector: Injector) {}

noop(): ScrollStrategy {
return new NoopScrollStrategy();
};

reposition(): ScrollStrategy {
return new EvoRepositionScrollStrategy(this.injector);
};

close(params?: EvoCloseScrollStrategyParams): ScrollStrategy {
return new EvoCloseScrollStrategy(this.injector, params);
};
}
5 changes: 5 additions & 0 deletions projects/evo-ui-kit/src/lib/common/scroll/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './types/evo-scroll-strategy';

export * from './classes/evo-scroll-strategy-options';
export * from './classes/evo-close-scroll-strategy';
export * from './classes/evo-reposition-scroll-strategy';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {ElementRef} from '@angular/core';

export interface EvoCloseScrollStrategyParams {
/** Amount of pixels the user has to scroll before the overlay is closed. */
threshold: number;
triggerRef: ElementRef;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ScrollPosition {
vertical: number;
horizontal: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type EvoScrollStrategy = 'noop' | 'close' | 'reposition';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {animationFrameScheduler, fromEvent, Observable} from 'rxjs';
import {filter, throttleTime} from 'rxjs/operators';
import {OverlayRef} from '@angular/cdk/overlay';

export function createScrollStream(document: Document, overlayRef: OverlayRef): Observable<Event> {
return fromEvent(document, 'scroll', {capture: true, passive: true}).pipe(
throttleTime(10, animationFrameScheduler, {leading: true, trailing: true}),
filter((event) => {
const target = event.target as Node;

return !overlayRef.overlayElement.contains(target);
}),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayOrigin]="dropdownOrigin"
[cdkConnectedOverlayPositions]="connectedPositions"
[cdkConnectedOverlayScrollStrategy]="scrollStrategy$ | async"
(detach)="close()"
(overlayOutsideClick)="onOverlayOutsideClick($event)"
>
Expand Down
Loading
Loading