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:
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user