From cddbe90645033d24fa0339caff01451169ca2097 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Thu, 18 Dec 2025 10:20:34 -0500 Subject: [PATCH] return focus to trigger without passing element --- .../vault/individual-vault/vault.component.ts | 20 +++++++------------ libs/components/src/dialog/dialog.service.ts | 8 +++++++- .../src/dialog/dialog/dialog.component.html | 3 ++- .../src/dialog/dialog/dialog.component.ts | 16 ++++++++++++++- .../src/menu/menu-trigger-for.directive.ts | 6 ++++++ .../src/tooltip/tooltip.directive.ts | 14 ++++++++++++- 6 files changed, 50 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index d4216f7b1a1..58fa59faec2 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -964,18 +964,12 @@ export class VaultComponent implements OnInit, OnDestr formConfig: CipherFormConfig, activeCollectionId?: CollectionId, ) { - this.vaultItemDialogRef = VaultItemDialogComponent.open( - this.dialogService, - { - mode, - formConfig, - activeCollectionId, - restore: this.restore, - }, - { - restoreFocus: this.newButtonEl(), - }, - ); + this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, { + mode, + formConfig, + activeCollectionId, + restore: this.restore, + }); const result = await lastValueFrom(this.vaultItemDialogRef.closed); this.vaultItemDialogRef = undefined; @@ -1145,7 +1139,7 @@ export class VaultComponent implements OnInit, OnDestr initialTab: tab, limitNestedCollections: true, }, - restoreFocus: this.newButtonEl(), + // Don't specify restoreFocus - let it default to capturing the currently focused element }); const result = await lastValueFrom(dialog.closed); diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index ab8d7e3fb77..7753ae15774 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -237,11 +237,17 @@ export class DialogService { backdropClass: this.backDropClasses, scrollStrategy: this.defaultScrollStrategy, positionStrategy: config?.positionStrategy ?? new ResponsivePositionStrategy(), + restoreFocus: true, injector, ...config, }; - ref.cdkDialogRefBase = this.dialog.open(componentOrTemplateRef, _config); + // Delay dialog opening to allow menus to close and restore focus to their triggers. + // This ensures proper focus restoration when dialogs opened from menus are closed. + setTimeout(() => { + ref.cdkDialogRefBase = this.dialog.open(componentOrTemplateRef, _config); + }, 0); + return ref; } diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 22aa99c44cb..9507d0d26a0 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -1,12 +1,13 @@ @let isDrawer = dialogRef?.isDrawer;
@let showHeaderBorder = bodyHasScrolledFrom().top;
>("scrollBottom"); + private readonly dialogSection = viewChild.required>("dialogSection"); protected dialogRef = inject(DialogRef, { optional: true }); protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody); @@ -141,4 +143,16 @@ export class DialogComponent { onAnimationEnd() { this.animationCompleted.set(true); } + + ngAfterViewInit() { + // Use setTimeout to ensure focus is captured after Angular's change detection + // and the dialog's focus trap has been initialized + setTimeout(() => { + const section = this.dialogSection().nativeElement; + const focusableElement = section.querySelector("[cdkFocusInitial]") as HTMLElement; + if (focusableElement && document.activeElement !== focusableElement) { + focusableElement.focus(); + } + }, 0); + } } diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index 1d79fbc9768..cb24fe17487 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -185,6 +185,12 @@ export class MenuTriggerForDirective implements OnDestroy { this.isOpen = false; this.disposeAll(); this.menu().closed.emit(); + + // Restore focus to the trigger button when the menu closes. + // This ensures that when a dialog opens from a menu item, the CDK Dialog's + // restoreFocus will capture the menu trigger button as the element to restore to. + // The dialog's ngAfterViewInit will then explicitly move focus into the dialog. + this.elementRef.nativeElement.focus(); } private setupClosingActions(isContextMenu: boolean) { diff --git a/libs/components/src/tooltip/tooltip.directive.ts b/libs/components/src/tooltip/tooltip.directive.ts index cca52526c7d..da978d230f8 100644 --- a/libs/components/src/tooltip/tooltip.directive.ts +++ b/libs/components/src/tooltip/tooltip.directive.ts @@ -51,6 +51,7 @@ export class TooltipDirective implements OnInit { private readonly isVisible = signal(false); private overlayRef: OverlayRef | undefined; + private showTimeoutId: ReturnType | undefined; private elementRef = inject>(ElementRef); private overlay = inject(Overlay); private viewContainerRef = inject(ViewContainerRef); @@ -82,12 +83,22 @@ export class TooltipDirective implements OnInit { ); private destroyTooltip = () => { + // Clear any pending show timeout to prevent tooltip from appearing after hide + if (this.showTimeoutId !== undefined) { + clearTimeout(this.showTimeoutId); + this.showTimeoutId = undefined; + } this.overlayRef?.dispose(); this.overlayRef = undefined; this.isVisible.set(false); }; protected showTooltip = () => { + // Clear any existing timeout before starting a new one + if (this.showTimeoutId !== undefined) { + clearTimeout(this.showTimeoutId); + } + if (!this.overlayRef) { this.overlayRef = this.overlay.create({ ...this.defaultPopoverConfig, @@ -97,8 +108,9 @@ export class TooltipDirective implements OnInit { this.overlayRef.attach(this.tooltipPortal); } - setTimeout(() => { + this.showTimeoutId = setTimeout(() => { this.isVisible.set(true); + this.showTimeoutId = undefined; }, TOOLTIP_DELAY_MS); };