mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-07 19:07:45 +01:00
[PM-12743] a11y changes to make new drop down list for send and vault accessible (#11717)
* updating new menus to allow tab + enter to submit the link/button * Updating New actions to use button instead of a for accessibiity purposes * refactor * refactor * test fix * fixes * fixing tests * fixing test * fixing tests --------- Co-authored-by: --global <>
This commit is contained in:
parent
456c516a6e
commit
0ff48aa345
@ -3,19 +3,27 @@
|
|||||||
{{ "new" | i18n }}
|
{{ "new" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<bit-menu #itemOptions>
|
<bit-menu #itemOptions>
|
||||||
<a bitMenuItem (click)="newItemNavigate(cipherType.Login)">
|
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.Login)">
|
||||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
||||||
{{ "typeLogin" | i18n }}
|
{{ "typeLogin" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a bitMenuItem (click)="newItemNavigate(cipherType.Card)">
|
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.Card)">
|
||||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
||||||
{{ "typeCard" | i18n }}
|
{{ "typeCard" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a bitMenuItem (click)="newItemNavigate(cipherType.Identity)">
|
<a
|
||||||
|
bitMenuItem
|
||||||
|
[routerLink]="['/add-cipher']"
|
||||||
|
[queryParams]="buildQueryParams(cipherType.Identity)"
|
||||||
|
>
|
||||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
||||||
{{ "typeIdentity" | i18n }}
|
{{ "typeIdentity" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a bitMenuItem (click)="newItemNavigate(cipherType.SecureNote)">
|
<a
|
||||||
|
bitMenuItem
|
||||||
|
[routerLink]="['/add-cipher']"
|
||||||
|
[queryParams]="buildQueryParams(cipherType.SecureNote)"
|
||||||
|
>
|
||||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
||||||
{{ "note" | i18n }}
|
{{ "note" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,141 +1,163 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { Router } from "@angular/router";
|
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components";
|
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
|
||||||
import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component";
|
|
||||||
|
|
||||||
import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component";
|
import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component";
|
||||||
|
|
||||||
describe("NewItemDropdownV2Component", () => {
|
describe("NewItemDropdownV2Component", () => {
|
||||||
let component: NewItemDropdownV2Component;
|
let component: NewItemDropdownV2Component;
|
||||||
let fixture: ComponentFixture<NewItemDropdownV2Component>;
|
let fixture: ComponentFixture<NewItemDropdownV2Component>;
|
||||||
const open = jest.fn();
|
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||||
const navigate = jest.fn();
|
let browserApiMock: jest.Mocked<typeof BrowserApi>;
|
||||||
|
|
||||||
jest
|
const mockTab = { url: "https://example.com" };
|
||||||
.spyOn(BrowserApi, "getTabFromCurrentWindow")
|
|
||||||
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
|
beforeAll(() => {
|
||||||
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(mockTab as chrome.tabs.Tab);
|
||||||
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
||||||
|
jest.spyOn(Utils, "getHostname").mockReturnValue("example.com");
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
open.mockClear();
|
dialogServiceMock = mock<DialogService>();
|
||||||
navigate.mockClear();
|
dialogServiceMock.open.mockClear();
|
||||||
|
|
||||||
|
const activatedRouteMock = {
|
||||||
|
snapshot: { paramMap: { get: jest.fn() } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const i18nServiceMock = mock<I18nService>();
|
||||||
|
const folderServiceMock = mock<FolderService>();
|
||||||
|
const folderApiServiceAbstractionMock = mock<FolderApiServiceAbstraction>();
|
||||||
|
const accountServiceMock = mock<AccountService>();
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NewItemDropdownV2Component, MenuModule, ButtonModule, JslibModule, CommonModule],
|
imports: [
|
||||||
providers: [
|
CommonModule,
|
||||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
RouterLink,
|
||||||
{ provide: Router, useValue: { navigate } },
|
ButtonModule,
|
||||||
|
MenuModule,
|
||||||
|
NoItemsModule,
|
||||||
|
NewItemDropdownV2Component,
|
||||||
],
|
],
|
||||||
})
|
providers: [
|
||||||
.overrideProvider(DialogService, { useValue: { open } })
|
{ provide: DialogService, useValue: dialogServiceMock },
|
||||||
.compileComponents();
|
{ provide: I18nService, useValue: i18nServiceMock },
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
||||||
|
{ provide: BrowserApi, useValue: browserApiMock },
|
||||||
|
{ provide: FolderService, useValue: folderServiceMock },
|
||||||
|
{ provide: FolderApiServiceAbstraction, useValue: folderApiServiceAbstractionMock },
|
||||||
|
{ provide: AccountService, useValue: accountServiceMock },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NewItemDropdownV2Component);
|
fixture = TestBed.createComponent(NewItemDropdownV2Component);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens new folder dialog", () => {
|
describe("buildQueryParams", () => {
|
||||||
component.openFolderDialog();
|
it("should build query params for a Login cipher when not popped out", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("new item", () => {
|
|
||||||
const emptyParams: AddEditQueryParams = {
|
|
||||||
collectionId: undefined,
|
|
||||||
organizationId: undefined,
|
|
||||||
folderId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.spyOn(component, "newItemNavigate");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("navigates to new login", async () => {
|
|
||||||
await component.newItemNavigate(CipherType.Login);
|
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
|
|
||||||
queryParams: {
|
|
||||||
type: CipherType.Login.toString(),
|
|
||||||
name: "example.com",
|
|
||||||
uri: "https://example.com",
|
|
||||||
...emptyParams,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("navigates to new card", async () => {
|
|
||||||
await component.newItemNavigate(CipherType.Card);
|
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
|
|
||||||
queryParams: { type: CipherType.Card.toString(), ...emptyParams },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("navigates to new identity", async () => {
|
|
||||||
await component.newItemNavigate(CipherType.Identity);
|
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
|
|
||||||
queryParams: { type: CipherType.Identity.toString(), ...emptyParams },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("navigates to new note", async () => {
|
|
||||||
await component.newItemNavigate(CipherType.SecureNote);
|
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
|
|
||||||
queryParams: { type: CipherType.SecureNote.toString(), ...emptyParams },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes initial values", async () => {
|
|
||||||
component.initialValues = {
|
component.initialValues = {
|
||||||
folderId: "222-333-444",
|
folderId: "222-333-444",
|
||||||
organizationId: "444-555-666",
|
organizationId: "444-555-666",
|
||||||
collectionId: "777-888-999",
|
collectionId: "777-888-999",
|
||||||
} as NewItemInitialValues;
|
} as NewItemInitialValues;
|
||||||
|
|
||||||
await component.newItemNavigate(CipherType.Login);
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
|
||||||
|
jest.spyOn(Utils, "getHostname").mockReturnValue("example.com");
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
|
const params = component.buildQueryParams(CipherType.Login);
|
||||||
queryParams: {
|
|
||||||
type: CipherType.Login.toString(),
|
expect(params).toEqual({
|
||||||
folderId: "222-333-444",
|
type: CipherType.Login.toString(),
|
||||||
organizationId: "444-555-666",
|
collectionId: "777-888-999",
|
||||||
collectionId: "777-888-999",
|
organizationId: "444-555-666",
|
||||||
uri: "https://example.com",
|
folderId: "222-333-444",
|
||||||
name: "example.com",
|
uri: "https://example.com",
|
||||||
},
|
name: "example.com",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not include name or uri when the extension is popped out", async () => {
|
it("should build query params for a Login cipher when popped out", () => {
|
||||||
|
component.initialValues = {
|
||||||
|
collectionId: "777-888-999",
|
||||||
|
} as NewItemInitialValues;
|
||||||
|
|
||||||
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
|
||||||
|
|
||||||
|
const params = component.buildQueryParams(CipherType.Login);
|
||||||
|
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: CipherType.Login.toString(),
|
||||||
|
collectionId: "777-888-999",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should build query params for a secure note", () => {
|
||||||
component.initialValues = {
|
component.initialValues = {
|
||||||
folderId: "222-333-444",
|
|
||||||
organizationId: "444-555-666",
|
|
||||||
collectionId: "777-888-999",
|
collectionId: "777-888-999",
|
||||||
} as NewItemInitialValues;
|
} as NewItemInitialValues;
|
||||||
|
|
||||||
await component.newItemNavigate(CipherType.Login);
|
const params = component.buildQueryParams(CipherType.SecureNote);
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
|
expect(params).toEqual({
|
||||||
queryParams: {
|
type: CipherType.SecureNote.toString(),
|
||||||
type: CipherType.Login.toString(),
|
collectionId: "777-888-999",
|
||||||
folderId: "222-333-444",
|
});
|
||||||
organizationId: "444-555-666",
|
});
|
||||||
collectionId: "777-888-999",
|
|
||||||
},
|
it("should build query params for an Identity", () => {
|
||||||
|
component.initialValues = {
|
||||||
|
collectionId: "777-888-999",
|
||||||
|
} as NewItemInitialValues;
|
||||||
|
|
||||||
|
const params = component.buildQueryParams(CipherType.Identity);
|
||||||
|
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: CipherType.Identity.toString(),
|
||||||
|
collectionId: "777-888-999",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should build query params for a Card", () => {
|
||||||
|
component.initialValues = {
|
||||||
|
collectionId: "777-888-999",
|
||||||
|
} as NewItemInitialValues;
|
||||||
|
|
||||||
|
const params = component.buildQueryParams(CipherType.Card);
|
||||||
|
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: CipherType.Card.toString(),
|
||||||
|
collectionId: "777-888-999",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should build query params for a SshKey", () => {
|
||||||
|
component.initialValues = {
|
||||||
|
collectionId: "777-888-999",
|
||||||
|
} as NewItemInitialValues;
|
||||||
|
|
||||||
|
const params = component.buildQueryParams(CipherType.SshKey);
|
||||||
|
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: CipherType.SshKey.toString(),
|
||||||
|
collectionId: "777-888-999",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
import { Router, RouterLink } from "@angular/router";
|
import { RouterLink } from "@angular/router";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@ -25,31 +25,31 @@ export interface NewItemInitialValues {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule],
|
imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule],
|
||||||
})
|
})
|
||||||
export class NewItemDropdownV2Component {
|
export class NewItemDropdownV2Component implements OnInit {
|
||||||
cipherType = CipherType;
|
cipherType = CipherType;
|
||||||
|
private tab?: chrome.tabs.Tab;
|
||||||
/**
|
/**
|
||||||
* Optional initial values to pass to the add cipher form
|
* Optional initial values to pass to the add cipher form
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
initialValues: NewItemInitialValues;
|
initialValues: NewItemInitialValues;
|
||||||
|
|
||||||
constructor(
|
constructor(private dialogService: DialogService) {}
|
||||||
private router: Router,
|
|
||||||
private dialogService: DialogService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private async buildQueryParams(type: CipherType): Promise<AddEditQueryParams> {
|
async ngOnInit() {
|
||||||
const tab = await BrowserApi.getTabFromCurrentWindow();
|
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildQueryParams(type: CipherType): AddEditQueryParams {
|
||||||
const poppedOut = BrowserPopupUtils.inPopout(window);
|
const poppedOut = BrowserPopupUtils.inPopout(window);
|
||||||
|
|
||||||
const loginDetails: { uri?: string; name?: string } = {};
|
const loginDetails: { uri?: string; name?: string } = {};
|
||||||
|
|
||||||
// When a Login Cipher is created and the extension is not popped out,
|
// When a Login Cipher is created and the extension is not popped out,
|
||||||
// pass along the uri and name
|
// pass along the uri and name
|
||||||
if (!poppedOut && type === CipherType.Login && tab) {
|
if (!poppedOut && type === CipherType.Login && this.tab) {
|
||||||
loginDetails.uri = tab.url;
|
loginDetails.uri = this.tab.url;
|
||||||
loginDetails.name = Utils.getHostname(tab.url);
|
loginDetails.name = Utils.getHostname(this.tab.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -61,10 +61,6 @@ export class NewItemDropdownV2Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async newItemNavigate(type: CipherType) {
|
|
||||||
await this.router.navigate(["/add-cipher"], { queryParams: await this.buildQueryParams(type) });
|
|
||||||
}
|
|
||||||
|
|
||||||
openFolderDialog() {
|
openFolderDialog() {
|
||||||
this.dialogService.open(AddEditFolderDialogComponent);
|
this.dialogService.open(AddEditFolderDialogComponent);
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,19 @@
|
|||||||
{{ (hideIcon ? "createSend" : "new") | i18n }}
|
{{ (hideIcon ? "createSend" : "new") | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<bit-menu #itemOptions>
|
<bit-menu #itemOptions>
|
||||||
<a type="button" bitMenuItem (click)="newItemNavigate(sendType.Text)">
|
<a
|
||||||
|
bitMenuItem
|
||||||
|
[routerLink]="buildRouterLink(sendType.File)"
|
||||||
|
[queryParams]="buildQueryParams(sendType.Text)"
|
||||||
|
>
|
||||||
<i class="bwi bwi-file-text" slot="start" aria-hidden="true"></i>
|
<i class="bwi bwi-file-text" slot="start" aria-hidden="true"></i>
|
||||||
{{ "sendTypeText" | i18n }}
|
{{ "sendTypeText" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a type="button" bitMenuItem (click)="newItemNavigate(sendType.File)">
|
<a
|
||||||
|
bitMenuItem
|
||||||
|
[routerLink]="buildRouterLink(sendType.File)"
|
||||||
|
[queryParams]="buildQueryParams(sendType.File)"
|
||||||
|
>
|
||||||
<i class="bwi bwi-file" slot="start" aria-hidden="true"></i>
|
<i class="bwi bwi-file" slot="start" aria-hidden="true"></i>
|
||||||
{{ "sendTypeFile" | i18n }}
|
{{ "sendTypeFile" | i18n }}
|
||||||
<button type="button" slot="end" *ngIf="hasNoPremium" bitBadge variant="success">
|
<button type="button" slot="end" *ngIf="hasNoPremium" bitBadge variant="success">
|
||||||
|
@ -32,10 +32,18 @@ export class NewSendDropdownComponent implements OnInit {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
newItemNavigate(type: SendType) {
|
buildRouterLink(type: SendType) {
|
||||||
if (this.hasNoPremium && type === SendType.File) {
|
if (this.hasNoPremium && type === SendType.File) {
|
||||||
return this.router.navigate(["/premium"]);
|
return "/premium";
|
||||||
|
} else {
|
||||||
|
return "/add-send";
|
||||||
}
|
}
|
||||||
void this.router.navigate(["/add-send"], { queryParams: { type: type, isNew: true } });
|
}
|
||||||
|
|
||||||
|
buildQueryParams(type: SendType) {
|
||||||
|
if (this.hasNoPremium && type === SendType.File) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { type: type, isNew: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user