From 6b823d402d219b8bcde48d172faad20b80cbe929 Mon Sep 17 00:00:00 2001 From: Nick Krantz Date: Fri, 23 Jan 2026 13:41:21 -0600 Subject: [PATCH] focus on dialog header when switching modes --- .../vault-item-dialog.component.spec.ts | 74 ++++++++++++++++++- .../vault-item-dialog.component.ts | 7 ++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts index 63b5071d1f5..c1251790227 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts @@ -104,7 +104,7 @@ describe("VaultItemDialogComponent", () => { getFeatureFlag$: () => of(false), }, }, - { provide: Router, useValue: {} }, + { provide: Router, useValue: { navigate: jest.fn() } }, { provide: ActivatedRoute, useValue: {} }, { provide: BillingAccountProfileStateService, @@ -337,4 +337,76 @@ describe("VaultItemDialogComponent", () => { }); }); }); + + describe("changeMode", () => { + beforeEach(() => { + component.setTestCipher({ type: CipherType.Login, id: "cipher-id" }); + }); + + it("refocuses the dialog header", async () => { + const focusOnHeaderSpy = jest.spyOn(component["dialogComponent"](), "focusOnHeader"); + + await component["changeMode"]("view"); + + expect(focusOnHeaderSpy).toHaveBeenCalled(); + }); + + describe("to view", () => { + beforeEach(() => { + component.setTestParams({ mode: "form" }); + fixture.detectChanges(); + }); + + it("sets mode to view", async () => { + await component["changeMode"]("view"); + + expect(component["params"].mode).toBe("view"); + }); + + it("updates the url", async () => { + const router = TestBed.inject(Router); + + await component["changeMode"]("view"); + + expect(router.navigate).toHaveBeenCalledWith([], { + queryParams: { action: "view", itemId: "cipher-id" }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + }); + }); + + describe("to form", () => { + const waitForFormReady = async () => { + const changeModePromise = component["changeMode"]("form"); + + expect(component["loadForm"]).toBe(true); + + component["onFormReady"](); + await changeModePromise; + }; + + beforeEach(() => { + component.setTestParams({ mode: "view" }); + fixture.detectChanges(); + }); + + it("waits for form to be ready when switching to form mode", async () => { + await waitForFormReady(); + + expect(component["params"].mode).toBe("form"); + }); + + it("updates the url", async () => { + const router = TestBed.inject(Router); + await waitForFormReady(); + + expect(router.navigate).toHaveBeenCalledWith([], { + queryParams: { action: "edit", itemId: "cipher-id" }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + }); + }); + }); }); 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 5d5e319c8af..b728a7cd2cc 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 @@ -8,6 +8,7 @@ import { Inject, OnDestroy, OnInit, + viewChild, ViewChild, } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -50,6 +51,7 @@ import { ItemModule, ToastService, CenterPositionStrategy, + DialogComponent, } from "@bitwarden/components"; import { AttachmentDialogCloseResult, @@ -172,6 +174,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CipherFormComponent) cipherFormComponent!: CipherFormComponent; + private readonly dialogComponent = viewChild(DialogComponent); + /** * Tracks if the cipher was ever modified while the dialog was open. Used to ensure the dialog emits the correct result * in case of closing with the X button or ESC key. @@ -693,6 +697,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { // Scroll to the top of the dialog content when switching modes. this.dialogContent.nativeElement.parentElement.scrollTop = 0; + // Refocus on title element, the built-in focus management of the dialog only works for the initial open. + this.dialogComponent().focusOnHeader(); + // Update the URL query params to reflect the new mode. await this.router.navigate([], { queryParams: {