1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00

[CL-672] update mobile design of dialog (#14828)

---------

Co-authored-by: Vicki League <vleague@bitwarden.com>
This commit is contained in:
Mark Youssef
2025-11-13 18:59:03 -08:00
committed by GitHub
parent 9a3ba9e05b
commit a55d0f02f2
30 changed files with 255 additions and 53 deletions

View File

@@ -5,7 +5,7 @@ import {
DIALOG_DATA,
DialogCloseOptions,
} from "@angular/cdk/dialog";
import { ComponentType, ScrollStrategy } from "@angular/cdk/overlay";
import { ComponentType, GlobalPositionStrategy, 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";
@@ -17,6 +17,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DrawerService } from "../drawer/drawer.service";
import { isAtOrLargerThanBreakpoint } from "../utils/responsive-utils";
import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component";
import { SimpleDialogOptions } from "./simple-dialog/types";
@@ -63,6 +64,68 @@ export type DialogConfig<D = unknown, R = unknown> = Pick<
"data" | "disableClose" | "ariaModal" | "positionStrategy" | "height" | "width"
>;
/**
* A responsive position strategy that adjusts the dialog position based on the screen size.
*/
class ResponsivePositionStrategy extends GlobalPositionStrategy {
private abortController: AbortController | null = null;
/**
* The previous breakpoint to avoid unnecessary updates.
* `null` means no previous breakpoint has been set.
*/
private prevBreakpoint: "small" | "large" | null = null;
constructor() {
super();
if (typeof window !== "undefined") {
this.abortController = new AbortController();
this.updatePosition(); // Initial position update
window.addEventListener("resize", this.updatePosition.bind(this), {
signal: this.abortController.signal,
});
}
}
override dispose() {
this.abortController?.abort();
this.abortController = null;
super.dispose();
}
updatePosition() {
const isSmallScreen = !isAtOrLargerThanBreakpoint("md");
const currentBreakpoint = isSmallScreen ? "small" : "large";
if (this.prevBreakpoint === currentBreakpoint) {
return; // No change in breakpoint, no need to update position
}
this.prevBreakpoint = currentBreakpoint;
if (isSmallScreen) {
this.bottom().centerHorizontally();
} else {
this.centerVertically().centerHorizontally();
}
this.apply();
}
}
/**
* Position strategy that centers dialogs regardless of screen size.
* Use this for simple dialogs and custom dialogs that should not use
* the responsive bottom-sheet behavior on mobile.
*
* @example
* dialogService.open(MyComponent, {
* positionStrategy: new CenterPositionStrategy()
* });
*/
export class CenterPositionStrategy extends GlobalPositionStrategy {
constructor() {
super();
this.centerHorizontally().centerVertically();
}
}
class DrawerDialogRef<R = unknown, C = unknown> implements DialogRef<R, C> {
readonly isDrawer = true;
@@ -172,6 +235,7 @@ export class DialogService {
const _config = {
backdropClass: this.backDropClasses,
scrollStrategy: this.defaultScrollStrategy,
positionStrategy: config?.positionStrategy ?? new ResponsivePositionStrategy(),
injector,
...config,
};
@@ -226,6 +290,7 @@ export class DialogService {
return this.open<boolean, SimpleDialogOptions>(SimpleConfigurableDialogComponent, {
data: simpleDialogOptions,
disableClose: simpleDialogOptions.disableClose,
positionStrategy: new CenterPositionStrategy(),
});
}

View File

@@ -1,8 +1,10 @@
@let isDrawer = dialogRef?.isDrawer;
<section
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-border tw-border-solid tw-border-secondary-100 tw-bg-background tw-text-main"
[ngClass]="[width, isDrawer ? 'tw-h-screen tw-border-t-0' : 'tw-rounded-xl tw-shadow-lg']"
@fadeIn
[ngClass]="[
width,
isDrawer ? 'tw-h-screen tw-border-t-0' : 'tw-rounded-t-xl md:tw-rounded-xl tw-shadow-lg',
]"
cdkTrapFocus
cdkTrapFocusAutoCapture
>

View File

@@ -3,13 +3,14 @@ import { CdkScrollable } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import {
Component,
HostBinding,
inject,
viewChild,
input,
booleanAttribute,
ElementRef,
DestroyRef,
computed,
signal,
} from "@angular/core";
import { toObservable } from "@angular/core/rxjs-interop";
import { combineLatest, switchMap } from "rxjs";
@@ -21,7 +22,6 @@ import { SpinnerComponent } from "../../spinner";
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";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
@@ -31,9 +31,10 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
@Component({
selector: "bit-dialog",
templateUrl: "./dialog.component.html",
animations: [fadeIn],
host: {
"[class]": "classes()",
"(keydown.esc)": "handleEsc($event)",
"(animationend)": "onAnimationEnd()",
},
imports: [
CommonModule,
@@ -87,22 +88,34 @@ export class DialogComponent {
*/
readonly disablePadding = input(false, { transform: booleanAttribute });
/**
* Disable animations for the dialog.
*/
readonly disableAnimations = input(false, { transform: booleanAttribute });
/**
* Mark the dialog as loading which replaces the content with a spinner.
*/
readonly loading = input(false);
@HostBinding("class") get classes() {
private readonly animationCompleted = signal(false);
protected readonly classes = computed(() => {
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
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();
}
const baseClasses = ["tw-flex", "tw-flex-col", "tw-w-screen"];
const sizeClasses = this.dialogRef?.isDrawer
? ["tw-min-h-screen", "md:tw-w-[23rem]"]
: ["md:tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"];
const animationClasses =
this.disableAnimations() || this.animationCompleted() || this.dialogRef?.isDrawer
? []
: this.dialogSize() === "small"
? ["tw-animate-slide-down"]
: ["tw-animate-slide-up", "md:tw-animate-slide-down"];
return [...baseClasses, this.width, ...sizeClasses, ...animationClasses];
});
handleEsc(event: Event) {
if (!this.dialogRef?.disableClose) {
@@ -124,4 +137,8 @@ export class DialogComponent {
}
}
}
onAnimationEnd() {
this.animationCompleted.set(true);
}
}

View File

@@ -57,6 +57,7 @@ export default {
args: {
loading: false,
dialogSize: "small",
disableAnimations: true,
},
argTypes: {
_disablePadding: {
@@ -71,6 +72,9 @@ export default {
defaultValue: "default",
},
},
disableAnimations: {
control: { type: "boolean" },
},
},
parameters: {
design: {
@@ -86,7 +90,7 @@ export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-dialog [dialogSize]="dialogSize" [title]="title" [subtitle]="subtitle" [loading]="loading" [disablePadding]="disablePadding">
<bit-dialog [dialogSize]="dialogSize" [title]="title" [subtitle]="subtitle" [loading]="loading" [disablePadding]="disablePadding" [disableAnimations]="disableAnimations">
<ng-container bitDialogTitle>
<span bitBadge variant="success">Foobar</span>
</ng-container>
@@ -158,7 +162,7 @@ export const ScrollingContent: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-dialog title="Scrolling Example" [background]="background" [dialogSize]="dialogSize" [loading]="loading" [disablePadding]="disablePadding">
<bit-dialog title="Scrolling Example" [background]="background" [dialogSize]="dialogSize" [loading]="loading" [disablePadding]="disablePadding" [disableAnimations]="disableAnimations">
<span bitDialogContent>
Dialog body text goes here.<br />
<ng-container *ngFor="let _ of [].constructor(100)">
@@ -175,6 +179,7 @@ export const ScrollingContent: Story = {
}),
args: {
dialogSize: "small",
disableAnimations: true,
},
};
@@ -182,7 +187,7 @@ export const TabContent: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-dialog title="Tab Content Example" [background]="background" [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<bit-dialog title="Tab Content Example" [background]="background" [dialogSize]="dialogSize" [disablePadding]="disablePadding" [disableAnimations]="disableAnimations">
<span bitDialogContent>
<bit-tab-group>
<bit-tab label="First Tab">First Tab Content</bit-tab>
@@ -200,6 +205,7 @@ export const TabContent: Story = {
args: {
dialogSize: "large",
disablePadding: true,
disableAnimations: true,
},
parameters: {
docs: {
@@ -219,7 +225,7 @@ export const WithCards: Story = {
},
template: /*html*/ `
<form [formGroup]="formObj">
<bit-dialog [dialogSize]="dialogSize" [background]="background" [title]="title" [subtitle]="subtitle" [loading]="loading" [disablePadding]="disablePadding">
<bit-dialog [dialogSize]="dialogSize" [background]="background" [title]="title" [subtitle]="subtitle" [loading]="loading" [disablePadding]="disablePadding" [disableAnimations]="disableAnimations">
<ng-container bitDialogContent>
<bit-section>
<bit-section-header>
@@ -283,5 +289,6 @@ export const WithCards: Story = {
title: "Default",
subtitle: "Subtitle",
background: "alt",
disableAnimations: true,
},
};

View File

@@ -9,7 +9,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { ButtonModule } from "../../button";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogModule } from "../dialog.module";
import { DialogService } from "../dialog.service";
import { CenterPositionStrategy, DialogService } from "../dialog.service";
interface Animal {
animal: string;
@@ -37,6 +37,7 @@ class StoryDialogComponent {
data: {
animal: "panda",
},
positionStrategy: new CenterPositionStrategy(),
});
}
@@ -46,6 +47,7 @@ class StoryDialogComponent {
animal: "panda",
},
disableClose: true,
positionStrategy: new CenterPositionStrategy(),
});
}
@@ -55,6 +57,7 @@ class StoryDialogComponent {
animal: "panda",
},
disableClose: true,
positionStrategy: new CenterPositionStrategy(),
});
}
}