1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

fix lifecycle bug by reworking scoll util

This commit is contained in:
William Martin
2025-04-23 17:22:02 -04:00
parent 4622873824
commit 797e793a11
4 changed files with 35 additions and 41 deletions

View File

@@ -6,7 +6,7 @@
cdkTrapFocus
cdkTrapFocusAutoCapture
>
@let showHeaderBorder = !isDrawer || bodyHasScrolledFrom?.top();
@let showHeaderBorder = !isDrawer || bodyHasScrolledFrom().top;
<header
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid"
[ngClass]="{
@@ -67,7 +67,7 @@
</div>
</div>
@let showFooterBorder = !isDrawer || bodyHasScrolledFrom?.bottom();
@let showFooterBorder = !isDrawer || bodyHasScrolledFrom().bottom;
<footer
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background"
[ngClass]="[isDrawer ? 'tw-px-6 tw-py-4' : 'tw-p-4']"

View File

@@ -4,21 +4,13 @@ import { CdkTrapFocus } from "@angular/cdk/a11y";
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { CdkScrollable } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import {
AfterViewInit,
Component,
HostBinding,
Injector,
Input,
inject,
viewChild,
} from "@angular/core";
import { Component, HostBinding, Input, inject, viewChild } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
import { BitIconButtonComponent } from "../../icon-button/icon-button.component";
import { TypographyDirective } from "../../typography/typography.directive";
import { ScrollState, hasScrolledFrom } from "../../utils/has-scrolled-from";
import { hasScrolledFrom } from "../../utils/has-scrolled-from";
import { fadeIn } from "../animations";
import { DialogRef } from "../dialog.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
@@ -43,11 +35,10 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
CdkScrollable,
],
})
export class DialogComponent implements AfterViewInit {
export class DialogComponent {
protected dialogRef = inject(DialogRef, { optional: true });
private scrollableBody = viewChild.required(CdkScrollable);
protected bodyHasScrolledFrom: ScrollState;
private injector = inject(Injector);
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
/** Background color */
@Input()
@@ -96,10 +87,6 @@ export class DialogComponent implements AfterViewInit {
.flat();
}
ngAfterViewInit(): void {
this.bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody(), this.injector);
}
handleEsc() {
this.dialogRef?.close();
}

View File

@@ -1,5 +1,5 @@
import { CdkScrollable } from "@angular/cdk/scrolling";
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { hasScrolledFrom } from "../utils/has-scrolled-from";
@@ -14,7 +14,7 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from";
host: {
class:
"tw-p-4 tw-pt-0 tw-block tw-overflow-auto tw-border-solid tw-border tw-border-transparent tw-transition-colors tw-duration-200",
"[class.tw-border-t-secondary-300]": "this.hasScrolledFrom.top()",
"[class.tw-border-t-secondary-300]": "this.hasScrolledFrom().top",
},
hostDirectives: [
{
@@ -24,5 +24,5 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from";
template: ` <ng-content></ng-content> `,
})
export class DrawerBodyComponent {
protected hasScrolledFrom = hasScrolledFrom(inject(CdkScrollable));
protected hasScrolledFrom = hasScrolledFrom();
}

View File

@@ -1,33 +1,40 @@
import { CdkScrollable } from "@angular/cdk/scrolling";
import { Injector, Signal, inject, runInInjectionContext } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { map } from "rxjs";
import { Signal, inject, signal } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { map, switchMap } from "rxjs";
export type ScrollState = {
/** `true` when the scrollbar is not at the top-most position */
top: Signal<boolean>;
top: boolean;
/** `true` when the scrollbar is not at the bottom-most position */
bottom: Signal<boolean>;
bottom: boolean;
};
/**
* Check if a `CdkScrollable` instance has been scrolled
* @param scrollable The element to check
* @param injector An optional injector; needed if called from outside an injection context
* @returns {ScrollState}
* @param scrollable The instance to check, defaults to the one provided by the current injector
* @returns {Signal<ScrollState>}
*/
export const hasScrolledFrom = (scrollable: CdkScrollable, injector?: Injector): ScrollState => {
const _injector = injector ?? inject(Injector);
const scrollState$ = scrollable.elementScrolled().pipe(
map(() => ({
top: scrollable.measureScrollOffset("top") > 0,
bottom: scrollable.measureScrollOffset("bottom") > 0,
})),
export const hasScrolledFrom = (scrollable?: Signal<CdkScrollable>): Signal<ScrollState> => {
const _scrollable = scrollable ?? signal(inject(CdkScrollable));
const scrollable$ = toObservable(_scrollable);
const scrollState$ = scrollable$.pipe(
switchMap((_scrollable) =>
_scrollable.elementScrolled().pipe(
map(() => ({
top: _scrollable.measureScrollOffset("top") > 0,
bottom: _scrollable.measureScrollOffset("bottom") > 0,
})),
),
),
);
return runInInjectionContext(_injector, () => ({
top: toSignal(scrollState$.pipe(map(($) => $.top)), { initialValue: false }),
bottom: toSignal(scrollState$.pipe(map(($) => $.bottom)), { initialValue: false }),
}));
return toSignal(scrollState$, {
initialValue: {
top: false,
bottom: false,
},
});
};