1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +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

@@ -1,7 +1,12 @@
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import {
ButtonModule,
CenterPositionStrategy,
DialogModule,
DialogService,
} from "@bitwarden/components";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@@ -11,6 +16,8 @@ import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components
})
export class AwaitDesktopDialogComponent {
static open(dialogService: DialogService) {
return dialogService.open<boolean>(AwaitDesktopDialogComponent);
return dialogService.open<boolean>(AwaitDesktopDialogComponent, {
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -9,6 +9,7 @@ import {
ButtonModule,
DialogModule,
DialogService,
CenterPositionStrategy,
} from "@bitwarden/components";
export type DesktopSyncVerificationDialogParams = {
@@ -49,6 +50,7 @@ export class DesktopSyncVerificationDialogComponent implements OnDestroy, OnInit
static open(dialogService: DialogService, data: DesktopSyncVerificationDialogParams) {
return dialogService.open(DesktopSyncVerificationDialogComponent, {
data,
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -3,7 +3,7 @@ import { Component, input, OnInit } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { DialogService } from "@bitwarden/components";
import { CenterPositionStrategy, DialogService } from "@bitwarden/components";
import { SendFormConfig } from "@bitwarden/send-ui";
import { FilePopoutUtilsService } from "../../services/file-popout-utils.service";
@@ -33,7 +33,9 @@ export class SendFilePopoutDialogContainerComponent implements OnInit {
this.config().mode === "add" &&
this.filePopoutUtilsService.showFilePopoutMessage(window)
) {
this.dialogService.open(SendFilePopoutDialogComponent);
this.dialogService.open(SendFilePopoutDialogComponent, {
positionStrategy: new CenterPositionStrategy(),
});
}
}
}

View File

@@ -7,7 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DeviceType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ItemModule } from "@bitwarden/components";
import { CenterPositionStrategy, DialogService, ItemModule } from "@bitwarden/components";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
@@ -51,7 +51,9 @@ export class AboutPageV2Component {
) {}
about() {
this.dialogService.open(AboutDialogComponent);
this.dialogService.open(AboutDialogComponent, {
positionStrategy: new CenterPositionStrategy(),
});
}
async launchHelp() {

View File

@@ -7,6 +7,7 @@ import {
DialogModule,
DialogService,
TypographyModule,
CenterPositionStrategy,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { DarkImageSourceDirective, VaultCarouselModule } from "@bitwarden/vault";
@@ -52,6 +53,7 @@ export class AtRiskCarouselDialogComponent {
static open(dialogService: DialogService) {
return dialogService.open<AtRiskCarouselDialogResult>(AtRiskCarouselDialogComponent, {
disableClose: true,
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -1,7 +1,13 @@
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import {
DIALOG_DATA,
ButtonModule,
DialogModule,
DialogService,
CenterPositionStrategy,
} from "@bitwarden/components";
export type BrowserSyncVerificationDialogParams = {
fingerprint: string[];
@@ -19,6 +25,7 @@ export class BrowserSyncVerificationDialogComponent {
static open(dialogService: DialogService, data: BrowserSyncVerificationDialogParams) {
return dialogService.open(BrowserSyncVerificationDialogComponent, {
data,
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -1,7 +1,13 @@
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import {
DIALOG_DATA,
ButtonModule,
DialogModule,
DialogService,
CenterPositionStrategy,
} from "@bitwarden/components";
export type VerifyNativeMessagingDialogData = {
applicationName: string;
@@ -19,6 +25,7 @@ export class VerifyNativeMessagingDialogComponent {
static open(dialogService: DialogService, data: VerifyNativeMessagingDialogData) {
return dialogService.open<boolean>(VerifyNativeMessagingDialogComponent, {
data,
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -9,7 +9,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { CenterPositionStrategy, DialogService } from "@bitwarden/components";
import { EncString } from "@bitwarden/sdk-internal";
import { SharedModule } from "../../../../shared";
@@ -58,7 +58,9 @@ export class vNextOrganizationDataOwnershipPolicyComponent
override async confirm(): Promise<boolean> {
if (this.policyResponse?.enabled && !this.enabled.value) {
const dialogRef = this.dialogService.open(this.warningContent);
const dialogRef = this.dialogService.open(this.warningContent, {
positionStrategy: new CenterPositionStrategy(),
});
const result = await lastValueFrom(dialogRef.closed);
return Boolean(result);
}

View File

@@ -25,7 +25,7 @@ const render: Story["render"] = (args) => ({
...args,
},
template: `
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding" disableAnimations>
<span bitDialogTitle>Access selector</span>
<span bitDialogContent>
<bit-access-selector

View File

@@ -28,7 +28,7 @@ class MockUpgradeNavButtonComponent {}
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
matches: true,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated

View File

@@ -16,6 +16,7 @@ import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"
import {
AnonLayoutWrapperDataService,
ButtonComponent,
CenterPositionStrategy,
DialogRef,
DialogService,
IconModule,
@@ -151,6 +152,7 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
data: {
onDismiss: this.dismissExtensionPage.bind(this),
},
positionStrategy: new CenterPositionStrategy(),
},
);
}

View File

@@ -48,6 +48,7 @@ import {
DialogService,
ItemModule,
ToastService,
CenterPositionStrategy,
} from "@bitwarden/components";
import {
AttachmentDialogCloseResult,
@@ -331,6 +332,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
if (this.cipher.decryptionFailure) {
this.dialogService.open(DecryptionFailureDialogComponent, {
data: { cipherIds: [this.cipher.id] },
positionStrategy: new CenterPositionStrategy(),
});
this.dialogRef.close();
return;

View File

@@ -14,6 +14,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherBulkDeleteRequest } from "@bitwarden/common/vault/models/request/cipher-bulk-delete.request";
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import {
CenterPositionStrategy,
DIALOG_DATA,
DialogConfig,
DialogRef,
@@ -48,7 +49,10 @@ export const openBulkDeleteDialog = (
) => {
return dialogService.open<BulkDeleteDialogResult, BulkDeleteDialogParams>(
BulkDeleteDialogComponent,
config,
{
positionStrategy: new CenterPositionStrategy(),
...config,
},
);
};

View File

@@ -26,7 +26,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { CenterPositionStrategy, DialogService } from "@bitwarden/components";
import { OrganizationCounts } from "../models/view/counts.view";
import { ProjectListView } from "../models/view/project-list.view";
@@ -341,6 +341,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
data: {
secrets: event,
},
positionStrategy: new CenterPositionStrategy(),
});
}

View File

@@ -21,7 +21,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { CenterPositionStrategy, DialogService } from "@bitwarden/components";
import { ProjectView } from "../../models/view/project.view";
import { SecretListView } from "../../models/view/secret-list.view";
@@ -126,6 +126,7 @@ export class ProjectSecretsComponent implements OnInit {
data: {
secrets: event,
},
positionStrategy: new CenterPositionStrategy(),
});
}

View File

@@ -19,6 +19,7 @@ import {
DialogService,
BitValidators,
ToastService,
CenterPositionStrategy,
} from "@bitwarden/components";
import { SecretAccessPoliciesView } from "../../models/view/access-policies/secret-access-policies.view";
@@ -225,6 +226,7 @@ export class SecretDialogComponent implements OnInit, OnDestroy {
data: {
secrets: secretListView,
},
positionStrategy: new CenterPositionStrategy(),
},
);

View File

@@ -13,7 +13,12 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
import {
CenterPositionStrategy,
DialogRef,
DialogService,
ToastService,
} from "@bitwarden/components";
import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component";
import { SecretListView } from "../models/view/secret-list.view";
@@ -180,6 +185,7 @@ export class SecretsComponent implements OnInit {
data: {
secrets: event,
},
positionStrategy: new CenterPositionStrategy(),
});
}

View File

@@ -6,7 +6,7 @@ import { combineLatestWith, Observable, startWith, switchMap } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { CenterPositionStrategy, DialogService } from "@bitwarden/components";
import { SecretListView } from "../models/view/secret-list.view";
import { SecretService } from "../secrets/secret.service";
@@ -64,6 +64,7 @@ export class TrashComponent implements OnInit {
secretIds: secretIds,
organizationId: this.organizationId,
},
positionStrategy: new CenterPositionStrategy(),
});
}
@@ -73,6 +74,7 @@ export class TrashComponent implements OnInit {
secretIds: secretIds,
organizationId: this.organizationId,
},
positionStrategy: new CenterPositionStrategy(),
});
}

View File

@@ -17,6 +17,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import {
ButtonModule,
ButtonType,
CenterPositionStrategy,
DialogModule,
DialogRef,
DialogService,
@@ -114,6 +115,8 @@ export class PremiumUpgradeDialogComponent {
* @returns A dialog reference object
*/
static open(dialogService: DialogService): DialogRef<PremiumUpgradeDialogComponent> {
return dialogService.open(PremiumUpgradeDialogComponent);
return dialogService.open(PremiumUpgradeDialogComponent, {
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -3,7 +3,13 @@ import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import {
DIALOG_DATA,
ButtonModule,
DialogModule,
DialogService,
CenterPositionStrategy,
} from "@bitwarden/components";
export type FingerprintDialogData = {
fingerprint: string[];
@@ -19,6 +25,9 @@ export class FingerprintDialogComponent {
constructor(@Inject(DIALOG_DATA) protected data: FingerprintDialogData) {}
static open(dialogService: DialogService, data: FingerprintDialogData) {
return dialogService.open(FingerprintDialogComponent, { data });
return dialogService.open(FingerprintDialogComponent, {
data,
positionStrategy: new CenterPositionStrategy(),
});
}
}

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(),
});
}
}

View File

@@ -2,32 +2,30 @@ import { Injectable } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, Observable, combineLatest, fromEvent, map, startWith } from "rxjs";
type CollapsePreference = "open" | "closed" | null;
import { BREAKPOINTS, isAtOrLargerThanBreakpoint } from "../utils/responsive-utils";
const SMALL_SCREEN_BREAKPOINT_PX = 768;
type CollapsePreference = "open" | "closed" | null;
@Injectable({
providedIn: "root",
})
export class SideNavService {
private _open$ = new BehaviorSubject<boolean>(
!window.matchMedia(`(max-width: ${SMALL_SCREEN_BREAKPOINT_PX}px)`).matches,
);
private _open$ = new BehaviorSubject<boolean>(isAtOrLargerThanBreakpoint("md"));
open$ = this._open$.asObservable();
private isSmallScreen$ = media(`(max-width: ${SMALL_SCREEN_BREAKPOINT_PX}px)`);
private isLargeScreen$ = media(`(min-width: ${BREAKPOINTS.md}px)`);
private _userCollapsePreference$ = new BehaviorSubject<CollapsePreference>(null);
userCollapsePreference$ = this._userCollapsePreference$.asObservable();
isOverlay$ = combineLatest([this.open$, this.isSmallScreen$]).pipe(
map(([open, isSmallScreen]) => open && isSmallScreen),
isOverlay$ = combineLatest([this.open$, this.isLargeScreen$]).pipe(
map(([open, isLargeScreen]) => open && !isLargeScreen),
);
constructor() {
combineLatest([this.isSmallScreen$, this.userCollapsePreference$])
combineLatest([this.isLargeScreen$, this.userCollapsePreference$])
.pipe(takeUntilDestroyed())
.subscribe(([isSmallScreen, userCollapsePreference]) => {
if (isSmallScreen) {
.subscribe(([isLargeScreen, userCollapsePreference]) => {
if (!isLargeScreen) {
this.setClose();
} else if (userCollapsePreference !== "closed") {
// Auto-open when user hasn't set preference (null) or prefers open

View File

@@ -0,0 +1,27 @@
/**
* Breakpoint definitions in pixels matching Tailwind CSS default breakpoints.
* These values must stay in sync with tailwind.config.base.js theme.extend configuration.
*
* @see {@link https://tailwindcss.com/docs/responsive-design} for tailwind default breakpoints
* @see {@link /libs/components/src/stories/responsive-design.mdx} for design system usage
*/
export const BREAKPOINTS = {
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
"2xl": 1536,
};
/**
* Checks if the current viewport is at or larger than the specified breakpoint.
* @param size The breakpoint to check.
* @returns True if the viewport is at or larger than the breakpoint, false otherwise.
*/
export const isAtOrLargerThanBreakpoint = (size: keyof typeof BREAKPOINTS): boolean => {
if (typeof window === "undefined" || !window.matchMedia) {
return false;
}
const query = `(min-width: ${BREAKPOINTS[size]}px)`;
return window.matchMedia(query).matches;
};

View File

@@ -167,6 +167,20 @@ module.exports = {
container: {
"@5xl": "1100px",
},
keyframes: {
slideUp: {
"0%": { opacity: "0", transform: "translateY(50px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
slideDown: {
"0%": { opacity: "0", transform: "translateY(-50px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
},
animation: {
"slide-up": "slideUp 0.3s ease-out",
"slide-down": "slideDown 0.3s ease-out",
},
},
},
plugins: [

View File

@@ -9,6 +9,7 @@ import {
DialogService,
DIALOG_DATA,
DialogRef,
CenterPositionStrategy,
} from "@bitwarden/components";
export type AdvancedUriOptionDialogParams = {
@@ -55,6 +56,7 @@ export class AdvancedUriOptionDialogComponent {
return dialogService.open<boolean>(AdvancedUriOptionDialogComponent, {
data: params,
disableClose: true,
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -13,6 +13,7 @@ import {
DialogModule,
DialogService,
TypographyModule,
CenterPositionStrategy,
} from "@bitwarden/components";
export type DecryptionFailureDialogParams = {
@@ -56,6 +57,9 @@ export class DecryptionFailureDialogComponent {
}
static open(dialogService: DialogService, params: DecryptionFailureDialogParams) {
return dialogService.open(DecryptionFailureDialogComponent, { data: params });
return dialogService.open(DecryptionFailureDialogComponent, {
data: params,
positionStrategy: new CenterPositionStrategy(),
});
}
}