From a55d0f02f23220e446cd38a48fa399806f8e9cb7 Mon Sep 17 00:00:00 2001 From: Mark Youssef <141061617+mark-youssef-bitwarden@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:59:03 -0800 Subject: [PATCH] [CL-672] update mobile design of dialog (#14828) --------- Co-authored-by: Vicki League --- .../await-desktop-dialog.component.ts | 11 ++- ...ktop-sync-verification-dialog.component.ts | 2 + ...-file-popout-dialog-container.component.ts | 6 +- .../about-page/about-page-v2.component.ts | 6 +- .../at-risk-carousel-dialog.component.ts | 2 + ...wser-sync-verification-dialog.component.ts | 9 ++- ...erify-native-messaging-dialog.component.ts | 9 ++- ...t-organization-data-ownership.component.ts | 6 +- .../access-selector-dialog.stories.ts | 2 +- .../navigation-switcher.component.spec.ts | 2 +- .../setup-extension.component.ts | 2 + .../vault-item-dialog.component.ts | 2 + .../bulk-delete-dialog.component.ts | 6 +- .../overview/overview.component.ts | 3 +- .../project/project-secrets.component.ts | 3 +- .../secrets/dialog/secret-dialog.component.ts | 2 + .../secrets/secrets.component.ts | 8 ++- .../secrets-manager/trash/trash.component.ts | 4 +- .../premium-upgrade-dialog.component.ts | 5 +- .../fingerprint-dialog.component.ts | 13 +++- libs/components/src/dialog/dialog.service.ts | 67 ++++++++++++++++++- .../src/dialog/dialog/dialog.component.html | 6 +- .../src/dialog/dialog/dialog.component.ts | 43 ++++++++---- .../src/dialog/dialog/dialog.stories.ts | 15 +++-- .../simple-dialog.service.stories.ts | 5 +- .../src/navigation/side-nav.service.ts | 20 +++--- libs/components/src/utils/responsive-utils.ts | 27 ++++++++ libs/components/tailwind.config.base.js | 14 ++++ .../advanced-uri-option-dialog.component.ts | 2 + .../decryption-failure-dialog.component.ts | 6 +- 30 files changed, 255 insertions(+), 53 deletions(-) create mode 100644 libs/components/src/utils/responsive-utils.ts diff --git a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts index a64cea1ef3e..12cf669d89b 100644 --- a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts +++ b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts @@ -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(AwaitDesktopDialogComponent); + return dialogService.open(AwaitDesktopDialogComponent, { + positionStrategy: new CenterPositionStrategy(), + }); } } diff --git a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts index 510348927ce..e1774dbbddd 100644 --- a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts +++ b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts @@ -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(), }); } } diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts index 56b8bcbb9f5..1f0d9f2a0c9 100644 --- a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts @@ -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(), + }); } } } diff --git a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.ts index 2ef830d9d94..88f6ad96807 100644 --- a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.ts @@ -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() { diff --git a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts index f81bccc760c..1b83c316f41 100644 --- a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts @@ -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(AtRiskCarouselDialogComponent, { disableClose: true, + positionStrategy: new CenterPositionStrategy(), }); } } diff --git a/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts b/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts index 5d3c777f333..d65df60a8ce 100644 --- a/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts +++ b/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts @@ -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(), }); } } diff --git a/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts b/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts index 14c2b137d73..6f9695f856a 100644 --- a/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts +++ b/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts @@ -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(VerifyNativeMessagingDialogComponent, { data, + positionStrategy: new CenterPositionStrategy(), }); } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts index a15c51ebf70..a0d425d5886 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts @@ -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 { 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); } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts index 5cb61197b99..3e23eff13a9 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts @@ -25,7 +25,7 @@ const render: Story["render"] = (args) => ({ ...args, }, template: ` - + Access selector ({ - matches: false, + matches: true, media: query, onchange: null, addListener: jest.fn(), // deprecated diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts index b5c0d096944..974e73bc91e 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -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(), }, ); } diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 98922fb114f..8508596a67b 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -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; diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 3856bb65324..5f139ade144 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -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( BulkDeleteDialogComponent, - config, + { + positionStrategy: new CenterPositionStrategy(), + ...config, + }, ); }; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index 12a5432c4b8..6995549e845 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -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(), }); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index 7112a28010f..9cd570a734a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -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(), }); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index 6376b58423d..53325fe2f54 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -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(), }, ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index 46cccb1d95d..92b33a06d4f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -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(), }); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts index b4da7769127..1e6483dcf92 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts @@ -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(), }); } diff --git a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.ts b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.ts index 48286a5d18c..ea9def03bd2 100644 --- a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.ts +++ b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.ts @@ -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 { - return dialogService.open(PremiumUpgradeDialogComponent); + return dialogService.open(PremiumUpgradeDialogComponent, { + positionStrategy: new CenterPositionStrategy(), + }); } } diff --git a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts index 6ef36a32448..5e4d1cbdb49 100644 --- a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts +++ b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts @@ -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(), + }); } } diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index 409bf0a5b55..1fc452418e1 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -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 = 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 implements DialogRef { 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(SimpleConfigurableDialogComponent, { data: simpleDialogOptions, disableClose: simpleDialogOptions.disableClose, + positionStrategy: new CenterPositionStrategy(), }); } diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 5774d83e349..83cfa21ed21 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -1,8 +1,10 @@ @let isDrawer = dialogRef?.isDrawer;
diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 71d594ef19e..954f03aabe2 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -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); + } } diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index d645d32764d..1f33ab7e877 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -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*/ ` - + Foobar @@ -158,7 +162,7 @@ export const ScrollingContent: Story = { render: (args) => ({ props: args, template: /*html*/ ` - + Dialog body text goes here.
@@ -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*/ ` - + First Tab Content @@ -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*/ `
- + @@ -283,5 +289,6 @@ export const WithCards: Story = { title: "Default", subtitle: "Subtitle", background: "alt", + disableAnimations: true, }, }; diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts index 5c94a959f25..b682b9f772a 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts @@ -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(), }); } } diff --git a/libs/components/src/navigation/side-nav.service.ts b/libs/components/src/navigation/side-nav.service.ts index 979cba1e3de..ce44811c7e0 100644 --- a/libs/components/src/navigation/side-nav.service.ts +++ b/libs/components/src/navigation/side-nav.service.ts @@ -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( - !window.matchMedia(`(max-width: ${SMALL_SCREEN_BREAKPOINT_PX}px)`).matches, - ); + private _open$ = new BehaviorSubject(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(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 diff --git a/libs/components/src/utils/responsive-utils.ts b/libs/components/src/utils/responsive-utils.ts new file mode 100644 index 00000000000..a9c2499c275 --- /dev/null +++ b/libs/components/src/utils/responsive-utils.ts @@ -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; +}; diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index ce399d860c1..e41cff16e48 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -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: [ diff --git a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts index f78c2c170f8..3580b1fada8 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts @@ -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(AdvancedUriOptionDialogComponent, { data: params, disableClose: true, + positionStrategy: new CenterPositionStrategy(), }); } } diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts index 628de79b3da..6b1a0e0d8aa 100644 --- a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts @@ -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(), + }); } }