mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[CL-834] Use intersection observer to determine if content scrolls (#16099)
* use intersection observer to fix dynamic content load issue * set up mock intersection observer * Create reusable hasScrollable content util * return null from resize to fix type error * remove Observer mock * return observable * refactor util and remove resize * use async pipe for observable in template * remove comment left in error
This commit is contained in:
@@ -65,9 +65,10 @@
|
||||
}"
|
||||
>
|
||||
<ng-content select="[bitDialogContent]"></ng-content>
|
||||
<div #scrollBottom></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@let isScrollable = isScrollable$ | async;
|
||||
@let showFooterBorder =
|
||||
(!bodyHasScrolledFrom().top && isScrollable) || bodyHasScrolledFrom().bottom;
|
||||
<footer
|
||||
|
||||
@@ -8,13 +8,17 @@ import {
|
||||
viewChild,
|
||||
input,
|
||||
booleanAttribute,
|
||||
AfterViewInit,
|
||||
ElementRef,
|
||||
DestroyRef,
|
||||
} from "@angular/core";
|
||||
import { toObservable } from "@angular/core/rxjs-interop";
|
||||
import { combineLatest, switchMap } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { BitIconButtonComponent } from "../../icon-button/icon-button.component";
|
||||
import { TypographyDirective } from "../../typography/typography.directive";
|
||||
import { hasScrollableContent$ } from "../../utils/";
|
||||
import { hasScrolledFrom } from "../../utils/has-scrolled-from";
|
||||
import { fadeIn } from "../animations";
|
||||
import { DialogRef } from "../dialog.service";
|
||||
@@ -39,11 +43,22 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
|
||||
CdkScrollable,
|
||||
],
|
||||
})
|
||||
export class DialogComponent implements AfterViewInit {
|
||||
protected dialogRef = inject(DialogRef, { optional: true });
|
||||
export class DialogComponent {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private scrollableBody = viewChild.required(CdkScrollable);
|
||||
private scrollBottom = viewChild.required<ElementRef<HTMLDivElement>>("scrollBottom");
|
||||
|
||||
protected dialogRef = inject(DialogRef, { optional: true });
|
||||
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
|
||||
protected isScrollable = false;
|
||||
|
||||
private scrollableBody$ = toObservable(this.scrollableBody);
|
||||
private scrollBottom$ = toObservable(this.scrollBottom);
|
||||
|
||||
protected isScrollable$ = combineLatest([this.scrollableBody$, this.scrollBottom$]).pipe(
|
||||
switchMap(([body, bottom]) =>
|
||||
hasScrollableContent$(body.getElementRef().nativeElement, bottom.nativeElement),
|
||||
),
|
||||
);
|
||||
|
||||
/** Background color */
|
||||
readonly background = input<"default" | "alt">("default");
|
||||
@@ -105,13 +120,4 @@ export class DialogComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.isScrollable = this.canScroll();
|
||||
}
|
||||
|
||||
canScroll(): boolean {
|
||||
const el = this.scrollableBody().getElementRef().nativeElement as HTMLElement;
|
||||
return el.scrollHeight > el.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
17
libs/components/src/utils/dom-observables.ts
Normal file
17
libs/components/src/utils/dom-observables.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
/** IntersectionObserver Observable */
|
||||
export const intersectionObserver$ = (
|
||||
target: Element,
|
||||
init: IntersectionObserverInit,
|
||||
): Observable<IntersectionObserverEntry> => {
|
||||
return new Observable((sub) => {
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
for (const e of entries) {
|
||||
sub.next(e);
|
||||
}
|
||||
}, init);
|
||||
io.observe(target);
|
||||
return () => io.disconnect();
|
||||
});
|
||||
};
|
||||
26
libs/components/src/utils/has-scrollable-content.ts
Normal file
26
libs/components/src/utils/has-scrollable-content.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Observable, animationFrameScheduler } from "rxjs";
|
||||
import { auditTime, map, startWith, observeOn, distinctUntilChanged } from "rxjs/operators";
|
||||
|
||||
import { intersectionObserver$ } from "./dom-observables";
|
||||
/**
|
||||
* Utility to determine if an element has scrollable content.
|
||||
* Returns an Observable that emits whenever scroll/resize/layout might change visibility
|
||||
*/
|
||||
export const hasScrollableContent$ = (
|
||||
root: HTMLElement,
|
||||
target: HTMLElement,
|
||||
threshold: number = 1,
|
||||
): Observable<boolean> => {
|
||||
return intersectionObserver$(target, { root, threshold }).pipe(
|
||||
startWith(null as IntersectionObserverEntry | null),
|
||||
auditTime(0, animationFrameScheduler),
|
||||
observeOn(animationFrameScheduler),
|
||||
map((entry: IntersectionObserverEntry | null) => {
|
||||
if (!entry) {
|
||||
return root.scrollHeight > root.clientHeight;
|
||||
}
|
||||
return !entry.isIntersecting;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./aria-disable-element";
|
||||
export * from "./function-to-observable";
|
||||
export * from "./has-scrollable-content";
|
||||
export * from "./i18n-mock.service";
|
||||
|
||||
Reference in New Issue
Block a user