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:
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
27
libs/components/src/utils/responsive-utils.ts
Normal file
27
libs/components/src/utils/responsive-utils.ts
Normal 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;
|
||||
};
|
||||
@@ -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: [
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user