mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 18:43:26 +00:00
open drawer as dialog
This commit is contained in:
@@ -1,31 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
DEFAULT_DIALOG_CONFIG,
|
||||
Dialog,
|
||||
Dialog as CdkDialog,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DIALOG_SCROLL_STRATEGY,
|
||||
DialogRef as CdkDialogRef,
|
||||
DIALOG_DATA,
|
||||
DialogCloseOptions,
|
||||
} from "@angular/cdk/dialog";
|
||||
import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay";
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
Injector,
|
||||
OnDestroy,
|
||||
Optional,
|
||||
SkipSelf,
|
||||
TemplateRef,
|
||||
} from "@angular/core";
|
||||
import { ComponentType, ScrollStrategy } from "@angular/cdk/overlay";
|
||||
import { ComponentPortal, Portal } from "@angular/cdk/portal";
|
||||
import { Injectable, InjectionToken, Injector, TemplateRef, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { filter, firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { DrawerService } from "../drawer/drawer.service";
|
||||
|
||||
import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component";
|
||||
import { SimpleDialogOptions, Translation } from "./simple-dialog/types";
|
||||
import { SimpleDialogOptions } from "./simple-dialog/types";
|
||||
|
||||
/**
|
||||
* The default `BlockScrollStrategy` does not work well with virtual scrolling.
|
||||
@@ -48,53 +42,74 @@ class CustomBlockScrollStrategy implements ScrollStrategy {
|
||||
detach() {}
|
||||
}
|
||||
|
||||
export const IS_DRAWER_TOKEN = new InjectionToken<boolean>("IS_DRAWER");
|
||||
|
||||
export abstract class DialogRef<R = unknown, C = unknown>
|
||||
implements Pick<CdkDialogRef<R, C>, "close" | "closed" | "disableClose" | "componentInstance">
|
||||
{
|
||||
abstract close(result?: R, options?: DialogCloseOptions): void;
|
||||
abstract closed: Observable<R | undefined>;
|
||||
abstract disableClose: boolean | undefined;
|
||||
/**
|
||||
* @deprecated
|
||||
* Does not work with drawer dialogs.
|
||||
**/
|
||||
abstract componentInstance: C | null;
|
||||
}
|
||||
|
||||
class DrawerDialogRef<R = unknown, C = unknown> implements DialogRef<R, C> {
|
||||
private _closed = new Subject<R | undefined>();
|
||||
closed = this._closed.asObservable();
|
||||
disableClose = false;
|
||||
|
||||
/** The portal containing the drawer */
|
||||
portal?: Portal<unknown>;
|
||||
|
||||
constructor(private drawerService: DrawerService) {}
|
||||
|
||||
close(result?: R, _options?: DialogCloseOptions): void {
|
||||
if (this.disableClose) {
|
||||
return;
|
||||
}
|
||||
this.drawerService.close(this.portal!);
|
||||
this._closed.next(result);
|
||||
this._closed.complete();
|
||||
}
|
||||
|
||||
componentInstance: C | null = null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DialogService extends Dialog implements OnDestroy {
|
||||
private _destroy$ = new Subject<void>();
|
||||
export class DialogService {
|
||||
private dialog = inject(CdkDialog);
|
||||
private drawerService = inject(DrawerService);
|
||||
private injector = inject(Injector);
|
||||
private router = inject(Router, { optional: true });
|
||||
private authService = inject(AuthService, { optional: true });
|
||||
private i18nService = inject(I18nService);
|
||||
|
||||
private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"];
|
||||
|
||||
private defaultScrollStrategy = new CustomBlockScrollStrategy();
|
||||
private activeDrawer: DrawerDialogRef<any, any> | null = null;
|
||||
|
||||
constructor(
|
||||
/** Parent class constructor */
|
||||
_overlay: Overlay,
|
||||
_injector: Injector,
|
||||
@Optional() @Inject(DEFAULT_DIALOG_CONFIG) _defaultOptions: DialogConfig,
|
||||
@Optional() @SkipSelf() _parentDialog: Dialog,
|
||||
_overlayContainer: OverlayContainer,
|
||||
@Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
|
||||
|
||||
/** Not in parent class */
|
||||
@Optional() router: Router,
|
||||
@Optional() authService: AuthService,
|
||||
|
||||
protected i18nService: I18nService,
|
||||
) {
|
||||
super(_overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, scrollStrategy);
|
||||
|
||||
constructor() {
|
||||
/** TODO: This logic should exist outside of `libs/components`. */
|
||||
/** Close all open dialogs if the vault locks */
|
||||
if (router && authService) {
|
||||
router.events
|
||||
if (this.router && this.authService) {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
switchMap(() => authService.getAuthStatus()),
|
||||
switchMap(() => this.authService!.getAuthStatus()),
|
||||
filter((v) => v !== AuthenticationStatus.Unlocked),
|
||||
takeUntil(this._destroy$),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(() => this.closeAll());
|
||||
}
|
||||
}
|
||||
|
||||
override ngOnDestroy(): void {
|
||||
this._destroy$.next();
|
||||
this._destroy$.complete();
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
override open<R = unknown, D = unknown, C = unknown>(
|
||||
open<R = unknown, D = unknown, C = any>(
|
||||
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
|
||||
config?: DialogConfig<D, DialogRef<R, C>>,
|
||||
config?: DialogConfig<D, CdkDialogRef<R, C>>,
|
||||
): DialogRef<R, C> {
|
||||
config = {
|
||||
backdropClass: this.backDropClasses,
|
||||
@@ -102,7 +117,44 @@ export class DialogService extends Dialog implements OnDestroy {
|
||||
...config,
|
||||
};
|
||||
|
||||
return super.open(componentOrTemplateRef, config);
|
||||
return this.dialog.open(componentOrTemplateRef, config);
|
||||
}
|
||||
|
||||
/** Opens a dialog in the side drawer */
|
||||
openDrawer<R = unknown, D = unknown, C = unknown>(
|
||||
component: ComponentType<C>,
|
||||
config?: DialogConfig<D, DialogRef<R, C>>,
|
||||
): DialogRef<R, C> {
|
||||
this.activeDrawer?.close();
|
||||
this.activeDrawer = new DrawerDialogRef(this.drawerService);
|
||||
const portal = new ComponentPortal(
|
||||
component,
|
||||
null,
|
||||
Injector.create({
|
||||
providers: [
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useValue: config?.data,
|
||||
},
|
||||
{
|
||||
provide: CdkDialogRef,
|
||||
useValue: this.activeDrawer,
|
||||
},
|
||||
{
|
||||
provide: DialogRef,
|
||||
useValue: this.activeDrawer,
|
||||
},
|
||||
{
|
||||
provide: IS_DRAWER_TOKEN,
|
||||
useValue: true,
|
||||
},
|
||||
],
|
||||
parent: this.injector,
|
||||
}),
|
||||
);
|
||||
this.activeDrawer.portal = portal;
|
||||
this.drawerService.open(portal);
|
||||
return this.activeDrawer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,8 +165,7 @@ export class DialogService extends Dialog implements OnDestroy {
|
||||
*/
|
||||
async openSimpleDialog(simpleDialogOptions: SimpleDialogOptions): Promise<boolean> {
|
||||
const dialogRef = this.openSimpleDialogRef(simpleDialogOptions);
|
||||
|
||||
return firstValueFrom(dialogRef.closed);
|
||||
return firstValueFrom(dialogRef.closed.pipe(map((v: boolean | undefined) => !!v)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,20 +185,8 @@ export class DialogService extends Dialog implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
protected translate(translation: string | Translation, defaultKey?: string): string {
|
||||
if (translation == null && defaultKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (translation == null) {
|
||||
return this.i18nService.t(defaultKey);
|
||||
}
|
||||
|
||||
// Translation interface use implies we must localize.
|
||||
if (typeof translation === "object") {
|
||||
return this.i18nService.t(translation.key, ...(translation.placeholders ?? []));
|
||||
}
|
||||
|
||||
return translation;
|
||||
/** Close all open dialogs */
|
||||
closeAll(): void {
|
||||
return this.dialog.closeAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<section
|
||||
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-text-main"
|
||||
[ngClass]="width"
|
||||
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-text-main"
|
||||
[ngClass]="[width, isDrawer ? 'tw-h-screen tw-border-t-0' : 'tw-rounded-xl']"
|
||||
@fadeIn
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
>
|
||||
<header
|
||||
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
|
||||
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
|
||||
[ngClass]="[isDrawer ? 'tw-py-2 tw-px-4' : 'tw-p-4'].flat()"
|
||||
>
|
||||
<h1
|
||||
<h2
|
||||
bitDialogTitleContainer
|
||||
bitTypography="h3"
|
||||
noMargin
|
||||
@@ -19,7 +22,7 @@
|
||||
</span>
|
||||
}
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
</h1>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
import { Component, HostBinding, Input, inject } from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { BitIconButtonComponent } from "../../icon-button/icon-button.component";
|
||||
import { TypographyDirective } from "../../typography/typography.directive";
|
||||
import { fadeIn } from "../animations";
|
||||
import { DialogRef, IS_DRAWER_TOKEN } from "../dialog.service";
|
||||
import { DialogCloseDirective } from "../directives/dialog-close.directive";
|
||||
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
|
||||
|
||||
@@ -17,6 +19,9 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
|
||||
templateUrl: "./dialog.component.html",
|
||||
animations: [fadeIn],
|
||||
standalone: true,
|
||||
host: {
|
||||
"(keydown.esc)": "handleEsc()",
|
||||
},
|
||||
imports: [
|
||||
CommonModule,
|
||||
DialogTitleContainerDirective,
|
||||
@@ -24,9 +29,13 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
|
||||
BitIconButtonComponent,
|
||||
DialogCloseDirective,
|
||||
I18nPipe,
|
||||
CdkTrapFocus,
|
||||
],
|
||||
})
|
||||
export class DialogComponent {
|
||||
private dialogRef = inject(DialogRef, { optional: true });
|
||||
protected isDrawer = inject(IS_DRAWER_TOKEN, { optional: true }) ?? false;
|
||||
|
||||
/** Background color */
|
||||
@Input()
|
||||
background: "default" | "alt" = "default";
|
||||
@@ -64,21 +73,28 @@ export class DialogComponent {
|
||||
|
||||
@HostBinding("class") get classes() {
|
||||
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
|
||||
return ["tw-flex", "tw-flex-col", "tw-w-screen", "tw-p-4", "tw-max-h-[90vh]"].concat(
|
||||
this.width,
|
||||
);
|
||||
return ["tw-flex", "tw-flex-col"]
|
||||
.concat(
|
||||
this.width,
|
||||
!this.isDrawer ? ["tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"] : ["tw-min-h-screen"],
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
|
||||
handleEsc() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
get width() {
|
||||
switch (this.dialogSize) {
|
||||
case "small": {
|
||||
return "tw-max-w-sm";
|
||||
return "md:tw-max-w-sm";
|
||||
}
|
||||
case "large": {
|
||||
return "tw-max-w-3xl";
|
||||
return "md:tw-max-w-3xl";
|
||||
}
|
||||
default: {
|
||||
return "tw-max-w-xl";
|
||||
return "md:tw-max-w-xl";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./dialog.module";
|
||||
export * from "./simple-dialog/types";
|
||||
export * from "./dialog.service";
|
||||
export { DialogConfig, DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
export { DialogConfig, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
|
||||
Reference in New Issue
Block a user