From c7cc3d60c98272afe66b62721876925b57455427 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 23 Apr 2025 13:12:10 -0400 Subject: [PATCH] add dynamic border styles to dialog template --- .../src/dialog/dialog/dialog.component.html | 18 ++++++++-- .../src/dialog/dialog/dialog.component.ts | 30 +++++++++++++---- .../src/drawer/drawer-body.component.ts | 18 +++------- .../components/kitchen-sink-main.component.ts | 2 +- .../components/src/utils/has-scrolled-from.ts | 33 +++++++++++++++++++ 5 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 libs/components/src/utils/has-scrolled-from.ts diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 28d97d74e04..413f170efef 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -6,9 +6,15 @@ cdkTrapFocus cdkTrapFocusAutoCapture > + @let showHeaderBorder = !isDrawer || bodyHasScrolledFrom?.top();

}
diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 22e3c7ee9db..76a2cfe344e 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -2,13 +2,23 @@ // @ts-strict-ignore import { CdkTrapFocus } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CdkScrollable } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { Component, HostBinding, Input, inject } from "@angular/core"; +import { + AfterViewInit, + Component, + HostBinding, + Injector, + 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 { fadeIn } from "../animations"; import { DialogRef } from "../dialog.service"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; @@ -30,10 +40,14 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai DialogCloseDirective, I18nPipe, CdkTrapFocus, + CdkScrollable, ], }) -export class DialogComponent { +export class DialogComponent implements AfterViewInit { protected dialogRef = inject(DialogRef, { optional: true }); + private scrollableBody = viewChild.required(CdkScrollable); + protected bodyHasScrolledFrom: ScrollState; + private injector = inject(Injector); /** Background color */ @Input() @@ -75,15 +89,19 @@ export class DialogComponent { return ["tw-flex", "tw-flex-col"] .concat( this.width, - !this.dialogRef.isDrawer - ? ["tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"] - : ["tw-min-h-screen"], + this.dialogRef?.isDrawer + ? ["tw-min-h-screen"] + : ["tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"], ) .flat(); } + ngAfterViewInit(): void { + this.bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody(), this.injector); + } + handleEsc() { - this.dialogRef.close(); + this.dialogRef?.close(); } get width() { diff --git a/libs/components/src/drawer/drawer-body.component.ts b/libs/components/src/drawer/drawer-body.component.ts index 9bd2adcffbc..e67704be5b0 100644 --- a/libs/components/src/drawer/drawer-body.component.ts +++ b/libs/components/src/drawer/drawer-body.component.ts @@ -1,7 +1,7 @@ import { CdkScrollable } from "@angular/cdk/scrolling"; -import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core"; -import { toSignal } from "@angular/core/rxjs-interop"; -import { map } from "rxjs"; +import { ChangeDetectionStrategy, Component, inject } from "@angular/core"; + +import { hasScrolledFrom } from "../utils/has-scrolled-from"; /** * Body container for `bit-drawer` @@ -14,7 +14,7 @@ import { map } from "rxjs"; 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]": "isScrolled()", + "[class.tw-border-t-secondary-300]": "this.hasScrolledFrom.top()", }, hostDirectives: [ { @@ -24,13 +24,5 @@ import { map } from "rxjs"; template: ` `, }) export class DrawerBodyComponent { - private scrollable = inject(CdkScrollable); - - /** TODO: share this utility with browser popup header? */ - protected isScrolled: Signal = toSignal( - this.scrollable - .elementScrolled() - .pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)), - { initialValue: false }, - ); + protected hasScrolledFrom = hasScrolledFrom(inject(CdkScrollable)); } diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index ecbf7892f38..bee9c910963 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -12,7 +12,7 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; standalone: true, imports: [KitchenSinkSharedModule], template: ` - +

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt diff --git a/libs/components/src/utils/has-scrolled-from.ts b/libs/components/src/utils/has-scrolled-from.ts new file mode 100644 index 00000000000..36f3fddc61e --- /dev/null +++ b/libs/components/src/utils/has-scrolled-from.ts @@ -0,0 +1,33 @@ +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"; + +export type ScrollState = { + /** `true` when the scrollbar is not at the top-most position */ + top: Signal; + + /** `true` when the scrollbar is not at the bottom-most position */ + bottom: Signal; +}; + +/** + * 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} + */ +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, + })), + ); + + return runInInjectionContext(_injector, () => ({ + top: toSignal(scrollState$.pipe(map(($) => $.top)), { initialValue: false }), + bottom: toSignal(scrollState$.pipe(map(($) => $.bottom)), { initialValue: false }), + })); +};