diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index 409bf0a5b55..a510e1e78bf 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -7,7 +7,7 @@ import { } from "@angular/cdk/dialog"; import { ComponentType, ScrollStrategy } from "@angular/cdk/overlay"; import { ComponentPortal, Portal } from "@angular/cdk/portal"; -import { Injectable, Injector, TemplateRef, inject } from "@angular/core"; +import { Injectable, Injector, TemplateRef, inject, signal } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import { filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs"; @@ -18,6 +18,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { DrawerService } from "../drawer/drawer.service"; +import { ANIMATION_IN_DURATION, ANIMATION_OUT_DURATION } from "./animations"; import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component"; import { SimpleDialogOptions } from "./simple-dialog/types"; @@ -132,6 +133,9 @@ export class DialogService { private defaultScrollStrategy = new CustomBlockScrollStrategy(); private activeDrawer: DrawerDialogRef | null = null; + /** Signal to control when focus trap auto-capture should be enabled after animation */ + readonly animationDone = signal(false); + constructor() { /** * TODO: This logic should exist outside of `libs/components`. @@ -177,6 +181,10 @@ export class DialogService { }; ref.cdkDialogRefBase = this.dialog.open(componentOrTemplateRef, _config); + + // Handle focus timing after animation completes + this.setupFocusTiming(); + return ref; } @@ -198,6 +206,10 @@ export class DialogService { ); this.activeDrawer.portal = portal; this.drawerService.open(portal); + + // Handle focus timing after animation completes + this.setupFocusTiming(); + return this.activeDrawer; } @@ -254,4 +266,20 @@ export class DialogService { parent: this.injector, }); } + + /** + * Sets up the focus timing for a dialog component after the animation completes. + * This ensures the focus trap and autofocus only activate after the entrance animation finishes. + */ + private setupFocusTiming(): void { + // Reset the signal to false for the new dialog + this.animationDone.set(false); + + const totalDuration = Math.max(ANIMATION_IN_DURATION, ANIMATION_OUT_DURATION); + + // Set to true after animation completes + setTimeout(() => { + this.animationDone.set(true); + }, totalDuration); + } } diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index f6f6746b3af..c66c984c63b 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -3,7 +3,6 @@ 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']" [@dialogAnimation]="isDrawer ? 'drawer' : 'dialog'" - (@dialogAnimation.done)="onAnimationDone()" cdkTrapFocus [cdkTrapFocusAutoCapture]="animationDone()" > diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index be092d6f4c3..2722be59536 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -9,7 +9,6 @@ import { input, booleanAttribute, ElementRef, - signal, } from "@angular/core"; import { toObservable } from "@angular/core/rxjs-interop"; import { combineLatest, switchMap } from "rxjs"; @@ -22,7 +21,7 @@ import { TypographyDirective } from "../../typography/typography.directive"; import { hasScrollableContent$ } from "../../utils/"; import { hasScrolledFrom } from "../../utils/has-scrolled-from"; import { dialogAnimation } from "../animations"; -import { DialogRef } from "../dialog.service"; +import { DialogRef, DialogService } from "../dialog.service"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; @@ -52,8 +51,9 @@ export class DialogComponent { private readonly scrollBottom = viewChild.required>("scrollBottom"); protected dialogRef = inject(DialogRef, { optional: true }); + private dialogService = inject(DialogService); protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody); - protected readonly animationDone = signal(false); + protected readonly animationDone = this.dialogService.animationDone; private scrollableBody$ = toObservable(this.scrollableBody); private scrollBottom$ = toObservable(this.scrollBottom); @@ -111,10 +111,6 @@ export class DialogComponent { } } - onAnimationDone() { - this.animationDone.set(true); - } - get width() { switch (this.dialogSize()) { case "small": { diff --git a/libs/components/src/switch/switch.module.ts b/libs/components/src/switch/switch.module.ts new file mode 100644 index 00000000000..714d451e6ee --- /dev/null +++ b/libs/components/src/switch/switch.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from "@angular/core"; + +import { SwitchComponent } from "./switch.component"; + +@NgModule({ + imports: [SwitchComponent], + exports: [SwitchComponent], +}) +export class SwitchModule {}