mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
[CL-696] un-revert "various drawer improvements" + bug fix (#14887)
* Revert "Revert "[CL-622][CL-562][CL-621][CL-632] various drawer improvements …"
This reverts commit 4b32d1f9dd.
* fix virtual scroll: add .cdk-virtual-scrollable to scroll viewport target
* remove references to main el
* use directives instead of querySelector (#14950)
* remove references to main el
* wip
* banish querySelector to the shadow realm
* revert apps/ files
* Add virtual scrolling docs
Co-authored-by: Vicki League <vleague@bitwarden.com>
* add jsdoc
* run eslint
* fix skip links bug
* Update libs/components/src/layout/layout.component.ts
Co-authored-by: Vicki League <vleague@bitwarden.com>
* update tab handler
* only run on tab
* fix lint
* fix virtual scroll issue due to Angular 19 upgrade (#15193)
thanks Vicki
---------
Co-authored-by: Vicki League <vleague@bitwarden.com>
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { provideAnimations } from "@angular/platform-browser/animations";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { NoopAnimationsModule, provideAnimations } from "@angular/platform-browser/animations";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { getAllByRole, userEvent } from "@storybook/test";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { LayoutComponent } from "../layout";
|
||||
import { SharedModule } from "../shared";
|
||||
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { DialogModule } from "./dialog.module";
|
||||
@@ -16,7 +22,12 @@ interface Animal {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<button bitButton type="button" (click)="openDialog()">Open Dialog</button>`,
|
||||
template: `
|
||||
<bit-layout>
|
||||
<button class="tw-mr-2" bitButton type="button" (click)="openDialog()">Open Dialog</button>
|
||||
<button bitButton type="button" (click)="openDrawer()">Open Drawer</button>
|
||||
</bit-layout>
|
||||
`,
|
||||
imports: [ButtonModule],
|
||||
})
|
||||
class StoryDialogComponent {
|
||||
@@ -29,6 +40,14 @@ class StoryDialogComponent {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openDrawer() {
|
||||
this.dialogService.openDrawer(StoryDialogContentComponent, {
|
||||
data: {
|
||||
animal: "panda",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -64,7 +83,21 @@ export default {
|
||||
title: "Component Library/Dialogs/Service",
|
||||
component: StoryDialogComponent,
|
||||
decorators: [
|
||||
positionFixedWrapperDecorator(),
|
||||
moduleMetadata({
|
||||
declarations: [StoryDialogContentComponent],
|
||||
imports: [
|
||||
SharedModule,
|
||||
ButtonModule,
|
||||
NoopAnimationsModule,
|
||||
DialogModule,
|
||||
IconButtonModule,
|
||||
RouterTestingModule,
|
||||
LayoutComponent,
|
||||
],
|
||||
providers: [DialogService],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
provideAnimations(),
|
||||
DialogService,
|
||||
@@ -73,7 +106,13 @@ export default {
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
close: "Close",
|
||||
loading: "Loading",
|
||||
search: "Search",
|
||||
skipToContent: "Skip to content",
|
||||
submenu: "submenu",
|
||||
toggleCollapse: "toggle collapse",
|
||||
toggleSideNavigation: "Toggle side navigation",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -90,4 +129,21 @@ export default {
|
||||
|
||||
type Story = StoryObj<StoryDialogComponent>;
|
||||
|
||||
export const Default: Story = {};
|
||||
export const Default: Story = {
|
||||
play: async (context) => {
|
||||
const canvas = context.canvasElement;
|
||||
|
||||
const button = getAllByRole(canvas, "button")[0];
|
||||
await userEvent.click(button);
|
||||
},
|
||||
};
|
||||
|
||||
/** Drawers must be a descendant of `bit-layout`. */
|
||||
export const Drawer: Story = {
|
||||
play: async (context) => {
|
||||
const canvas = context.canvasElement;
|
||||
|
||||
const button = getAllByRole(canvas, "button")[1];
|
||||
await userEvent.click(button);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DIALOG_SCROLL_STRATEGY,
|
||||
Dialog as CdkDialog,
|
||||
DialogConfig as CdkDialogConfig,
|
||||
DialogRef as CdkDialogRefBase,
|
||||
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, 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,61 +42,163 @@ class CustomBlockScrollStrategy implements ScrollStrategy {
|
||||
detach() {}
|
||||
}
|
||||
|
||||
export abstract class DialogRef<R = unknown, C = unknown>
|
||||
implements Pick<CdkDialogRef<R, C>, "close" | "closed" | "disableClose" | "componentInstance">
|
||||
{
|
||||
abstract readonly isDrawer?: boolean;
|
||||
|
||||
// --- From CdkDialogRef ---
|
||||
abstract close(result?: R, options?: DialogCloseOptions): void;
|
||||
abstract readonly closed: Observable<R | undefined>;
|
||||
abstract disableClose: boolean | undefined;
|
||||
/**
|
||||
* @deprecated
|
||||
* Does not work with drawer dialogs.
|
||||
**/
|
||||
abstract componentInstance: C | null;
|
||||
}
|
||||
|
||||
export type DialogConfig<D = unknown, R = unknown> = Pick<
|
||||
CdkDialogConfig<D, R>,
|
||||
"data" | "disableClose" | "ariaModal" | "positionStrategy" | "height" | "width"
|
||||
>;
|
||||
|
||||
class DrawerDialogRef<R = unknown, C = unknown> implements DialogRef<R, C> {
|
||||
readonly isDrawer = true;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* DialogRef that delegates functionality to the CDK implementation
|
||||
**/
|
||||
export class CdkDialogRef<R = unknown, C = unknown> implements DialogRef<R, C> {
|
||||
readonly isDrawer = false;
|
||||
|
||||
/** This is not available until after construction, @see DialogService.open. */
|
||||
cdkDialogRefBase!: CdkDialogRefBase<R, C>;
|
||||
|
||||
// --- Delegated to CdkDialogRefBase ---
|
||||
|
||||
close(result?: R, options?: DialogCloseOptions): void {
|
||||
this.cdkDialogRefBase.close(result, options);
|
||||
}
|
||||
|
||||
get closed(): Observable<R | undefined> {
|
||||
return this.cdkDialogRefBase.closed;
|
||||
}
|
||||
|
||||
get disableClose(): boolean | undefined {
|
||||
return this.cdkDialogRefBase.disableClose;
|
||||
}
|
||||
set disableClose(value: boolean | undefined) {
|
||||
this.cdkDialogRefBase.disableClose = value;
|
||||
}
|
||||
|
||||
// Delegate the `componentInstance` property to the CDK DialogRef
|
||||
get componentInstance(): C | null {
|
||||
return this.cdkDialogRefBase.componentInstance;
|
||||
}
|
||||
}
|
||||
|
||||
@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`.
|
||||
* @see https://bitwarden.atlassian.net/browse/CL-657
|
||||
**/
|
||||
/** 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 = unknown>(
|
||||
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
|
||||
config?: DialogConfig<D, DialogRef<R, C>>,
|
||||
): DialogRef<R, C> {
|
||||
config = {
|
||||
/**
|
||||
* This is a bit circular in nature:
|
||||
* We need the DialogRef instance for the DI injector that is passed *to* `Dialog.open`,
|
||||
* but we get the base CDK DialogRef instance *from* `Dialog.open`.
|
||||
*
|
||||
* To break the circle, we define CDKDialogRef as a wrapper for the CDKDialogRefBase.
|
||||
* This allows us to create the class instance and provide the base instance later, almost like "deferred inheritance".
|
||||
**/
|
||||
const ref = new CdkDialogRef<R, C>();
|
||||
const injector = this.createInjector({
|
||||
data: config?.data,
|
||||
dialogRef: ref,
|
||||
});
|
||||
|
||||
// Merge the custom config with the default config
|
||||
const _config = {
|
||||
backdropClass: this.backDropClasses,
|
||||
scrollStrategy: this.defaultScrollStrategy,
|
||||
injector,
|
||||
...config,
|
||||
};
|
||||
|
||||
return super.open(componentOrTemplateRef, config);
|
||||
ref.cdkDialogRefBase = this.dialog.open<R, D, C>(componentOrTemplateRef, _config);
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** 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 is also circular. When creating the DrawerDialogRef, we do not yet have a portal instance to provide to the injector.
|
||||
* Similar to `this.open`, we get around this with mutability.
|
||||
*/
|
||||
this.activeDrawer = new DrawerDialogRef(this.drawerService);
|
||||
const portal = new ComponentPortal(
|
||||
component,
|
||||
null,
|
||||
this.createInjector({ data: config?.data, dialogRef: this.activeDrawer }),
|
||||
);
|
||||
this.activeDrawer.portal = portal;
|
||||
this.drawerService.open(portal);
|
||||
return this.activeDrawer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,8 +209,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 +229,29 @@ export class DialogService extends Dialog implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
protected translate(translation: string | Translation, defaultKey?: string): string {
|
||||
if (translation == null && defaultKey == null) {
|
||||
return null;
|
||||
}
|
||||
/** Close all open dialogs */
|
||||
closeAll(): void {
|
||||
return this.dialog.closeAll();
|
||||
}
|
||||
|
||||
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;
|
||||
/** The injector that is passed to the opened dialog */
|
||||
private createInjector(opts: { data: unknown; dialogRef: DialogRef }): Injector {
|
||||
return Injector.create({
|
||||
providers: [
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useValue: opts.data,
|
||||
},
|
||||
{
|
||||
provide: DialogRef,
|
||||
useValue: opts.dialogRef,
|
||||
},
|
||||
{
|
||||
provide: CdkDialogRefBase,
|
||||
useValue: opts.dialogRef,
|
||||
},
|
||||
],
|
||||
parent: this.injector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
@let isDrawer = dialogRef?.isDrawer;
|
||||
<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
|
||||
>
|
||||
@let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
|
||||
<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"
|
||||
[ngClass]="{
|
||||
'tw-p-4': !isDrawer,
|
||||
'tw-p-6 tw-pb-4': isDrawer,
|
||||
'tw-border-secondary-300': showHeaderBorder,
|
||||
'tw-border-transparent': !showHeaderBorder,
|
||||
}"
|
||||
>
|
||||
<h1
|
||||
<h2
|
||||
bitDialogTitleContainer
|
||||
bitTypography="h3"
|
||||
noMargin
|
||||
@@ -19,7 +29,7 @@
|
||||
</span>
|
||||
}
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
</h1>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
@@ -32,9 +42,11 @@
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="tw-relative tw-flex tw-flex-col tw-overflow-hidden"
|
||||
class="tw-relative tw-flex-1 tw-flex tw-flex-col tw-overflow-hidden"
|
||||
[ngClass]="{
|
||||
'tw-min-h-60': loading,
|
||||
'tw-bg-background': background === 'default',
|
||||
'tw-bg-background-alt': background === 'alt',
|
||||
}"
|
||||
>
|
||||
@if (loading) {
|
||||
@@ -43,20 +55,28 @@
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
cdkScrollable
|
||||
[ngClass]="{
|
||||
'tw-p-4': !disablePadding,
|
||||
'tw-p-4': !disablePadding && !isDrawer,
|
||||
'tw-px-6 tw-py-4': !disablePadding && isDrawer,
|
||||
'tw-overflow-y-auto': !loading,
|
||||
'tw-invisible tw-overflow-y-hidden': loading,
|
||||
'tw-bg-background': background === 'default',
|
||||
'tw-bg-background-alt': background === 'alt',
|
||||
}"
|
||||
>
|
||||
<ng-content select="[bitDialogContent]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@let showFooterBorder = !isDrawer || background === "alt" || 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 tw-p-4"
|
||||
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']"
|
||||
[ngClass]="{
|
||||
'tw-px-6 tw-py-4': isDrawer,
|
||||
'tw-p-4': !isDrawer,
|
||||
'tw-border-secondary-300': showFooterBorder,
|
||||
'tw-border-transparent': !showFooterBorder,
|
||||
}"
|
||||
>
|
||||
<ng-content select="[bitDialogFooter]"></ng-content>
|
||||
</footer>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// 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 { CdkScrollable } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, HostBinding, Input } 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 { hasScrolledFrom } from "../../utils/has-scrolled-from";
|
||||
import { fadeIn } from "../animations";
|
||||
import { DialogRef } from "../dialog.service";
|
||||
import { DialogCloseDirective } from "../directives/dialog-close.directive";
|
||||
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
|
||||
|
||||
@@ -16,6 +20,9 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
|
||||
selector: "bit-dialog",
|
||||
templateUrl: "./dialog.component.html",
|
||||
animations: [fadeIn],
|
||||
host: {
|
||||
"(keydown.esc)": "handleEsc($event)",
|
||||
},
|
||||
imports: [
|
||||
CommonModule,
|
||||
DialogTitleContainerDirective,
|
||||
@@ -23,9 +30,15 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
|
||||
BitIconButtonComponent,
|
||||
DialogCloseDirective,
|
||||
I18nPipe,
|
||||
CdkTrapFocus,
|
||||
CdkScrollable,
|
||||
],
|
||||
})
|
||||
export class DialogComponent {
|
||||
protected dialogRef = inject(DialogRef, { optional: true });
|
||||
private scrollableBody = viewChild.required(CdkScrollable);
|
||||
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
|
||||
|
||||
/** Background color */
|
||||
@Input()
|
||||
background: "default" | "alt" = "default";
|
||||
@@ -63,21 +76,31 @@ 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", "tw-w-screen"]
|
||||
.concat(
|
||||
this.width,
|
||||
this.dialogRef?.isDrawer
|
||||
? ["tw-min-h-screen", "md:tw-w-[23rem]"]
|
||||
: ["tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"],
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
|
||||
handleEsc(event: Event) {
|
||||
this.dialogRef?.close();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ For alerts or simple confirmation actions, like speedbumps, use the
|
||||
Dialogs's should be used sparingly as they do call extra attention to themselves and can be
|
||||
interruptive if overused.
|
||||
|
||||
For non-blocking, supplementary content, open dialogs as a
|
||||
[Drawer](?path=/story/component-library-dialogs-service--drawer) (requires `bit-layout`).
|
||||
|
||||
## Placement
|
||||
|
||||
Dialogs should be centered vertically and horizontally on screen. Dialogs height should expand to
|
||||
|
||||
@@ -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 { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
|
||||
Reference in New Issue
Block a user