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 15e62eaf93e..1e0de4c3ef2 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 @@ -49,6 +49,7 @@ import { ItemModule, ToastService, CenterPositionStrategy, + DialogConfig, } from "@bitwarden/components"; import { AttachmentDialogCloseResult, @@ -667,10 +668,15 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { * @param dialogService * @param params */ - static open(dialogService: DialogService, params: VaultItemDialogParams) { + static open( + dialogService: DialogService, + params: VaultItemDialogParams, + dialogConfig?: DialogConfig, + ) { return dialogService.open( VaultItemDialogComponent, { + ...dialogConfig, data: params, }, ); diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 8fa801f5dc0..7ae01cbec6a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -1,5 +1,12 @@ import { CommonModule } from "@angular/common"; -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + viewChild, +} from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, switchMap } from "rxjs"; @@ -56,6 +63,9 @@ export class VaultHeaderComponent { protected CollectionDialogTabType = CollectionDialogTabType; protected CipherType = CipherType; + /** Query for the NewCipherMenuComponent in the template */ + readonly newCipherMenu = viewChild(NewCipherMenuComponent); + /** * Boolean to determine the loading state of the header. * Shows a loading spinner if set to true 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 e791ca7a90b..d4216f7b1a1 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,6 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { + ChangeDetectorRef, + Component, + computed, + NgZone, + OnDestroy, + OnInit, + viewChild, + ViewChild, +} from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; import { BehaviorSubject, @@ -195,6 +204,12 @@ export class VaultComponent implements OnInit, OnDestr // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; + readonly vaultHeaderComponent = viewChild(VaultHeaderComponent); + + readonly newButtonEl = computed( + () => this.vaultHeaderComponent()?.newCipherMenu()?.newCipherButton()?.el.nativeElement, + ); + trashCleanupWarning: string = null; kdfIterations: number; activeFilter: VaultFilter = new VaultFilter(); @@ -861,7 +876,9 @@ export class VaultComponent implements OnInit, OnDestr } addFolder = (): void => { - AddEditFolderDialogComponent.open(this.dialogService); + AddEditFolderDialogComponent.open(this.dialogService, undefined, { + restoreFocus: this.newButtonEl(), + }); }; editFolder = async (folder: FolderFilter): Promise => { @@ -947,12 +964,18 @@ export class VaultComponent implements OnInit, OnDestr formConfig: CipherFormConfig, activeCollectionId?: CollectionId, ) { - this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, { - mode, - formConfig, - activeCollectionId, - restore: this.restore, - }); + this.vaultItemDialogRef = VaultItemDialogComponent.open( + this.dialogService, + { + mode, + formConfig, + activeCollectionId, + restore: this.restore, + }, + { + restoreFocus: this.newButtonEl(), + }, + ); const result = await lastValueFrom(this.vaultItemDialogRef.closed); this.vaultItemDialogRef = undefined; @@ -1098,6 +1121,7 @@ export class VaultComponent implements OnInit, OnDestr showOrgSelector: true, limitNestedCollections: true, }, + restoreFocus: this.newButtonEl(), }); const result = await lastValueFrom(dialog.closed); if (result.action === CollectionDialogAction.Saved) { @@ -1121,6 +1145,7 @@ export class VaultComponent implements OnInit, OnDestr initialTab: tab, limitNestedCollections: true, }, + restoreFocus: this.newButtonEl(), }); const result = await lastValueFrom(dialog.closed); diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 7cae8fe974d..f9cfa19f153 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -148,7 +148,7 @@ export class ButtonComponent implements ButtonLikeAbstraction { ); readonly disabled = model(false); - private el = inject(ElementRef); + readonly el = inject(ElementRef); constructor() { ariaDisableElement(this.el.nativeElement, this.disabledAttr); diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index 804b650beab..ab8d7e3fb77 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -62,7 +62,7 @@ export abstract class DialogRef implements Pick< export type DialogConfig = Pick< CdkDialogConfig, - "data" | "disableClose" | "ariaModal" | "positionStrategy" | "height" | "width" + "data" | "disableClose" | "ariaModal" | "positionStrategy" | "height" | "width" | "restoreFocus" >; /** diff --git a/libs/components/src/menu/menu.mdx b/libs/components/src/menu/menu.mdx index 61e7b40ce00..2171fe6c22a 100644 --- a/libs/components/src/menu/menu.mdx +++ b/libs/components/src/menu/menu.mdx @@ -38,3 +38,8 @@ prior to being clicked and `aria-expanded="true"` after the user clicks the elem User should be able to navigate the opened menu via the up and down arrow keys and close the menu using the escape key or clicking out of the menu. + +Are you using a menu item to open a dialog? Be sure to pass the `restoreFocus` config option to the +`dialog.open` method, specifying where the user's focus should return to upon dialog close. (The +menu closes when the dialog launches, so the built-in strategy of returning focus to the trigger +element is not possible, since the trigger element menu item is gone.) diff --git a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts index adc4c67b2f4..42623bb0dd8 100644 --- a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts +++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts @@ -32,6 +32,7 @@ import { FormFieldModule, IconButtonModule, ToastService, + DialogConfig, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -175,10 +176,17 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit { this.dialogRef.close(result); } - static open(dialogService: DialogService, data?: AddEditFolderDialogData) { + static open( + dialogService: DialogService, + data?: AddEditFolderDialogData, + dialogConfig?: DialogConfig, + ) { return dialogService.open( AddEditFolderDialogComponent, - { data }, + { + ...dialogConfig, + data, + }, ); } } diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html index 00cfa701529..b368850dc3c 100644 --- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html @@ -7,6 +7,7 @@ [bitMenuTriggerFor]="addOptions" id="newItemDropdown" [appA11yTitle]="'new' | i18n" + #newItemDropdown > {{ "new" | i18n }} diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts index 0a755a9cdb4..9afb0b767ad 100644 --- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts @@ -1,12 +1,12 @@ import { CommonModule } from "@angular/common"; -import { Component, input, output } from "@angular/core"; +import { Component, input, output, viewChild } from "@angular/core"; import { map, shareReplay } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherType } from "@bitwarden/common/vault/enums"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; -import { ButtonModule, MenuModule } from "@bitwarden/components"; +import { ButtonComponent, ButtonModule, MenuModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush @@ -33,6 +33,8 @@ export class NewCipherMenuComponent { collectionAdded = output(); cipherAdded = output(); + readonly newCipherButton = viewChild("newItemDropdown"); + constructor(private restrictedItemTypesService: RestrictedItemTypesService) {} /**