[AccountSwitching]Make account switcher accessible (#1289)

* Make account switcher keyboard accessible

* ScreenReader: Announce submenu and expansion

* ScreenReader: Announc switch account button with account info

* Fix tab focus on dropdown

* Fix esc not changing state

* Fix linting issues

Co-authored-by: Hinton <oscar@oscarhinton.com>
This commit is contained in:
Daniel James Smith 2022-02-03 17:46:14 +01:00 committed by GitHub
parent 3e8705d548
commit 2b58861296
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 43 additions and 17 deletions

View File

@ -1,9 +1,12 @@
<a <button
class="account-switcher" class="account-switcher"
(click)="toggle()" (click)="toggle()"
cdkOverlayOrigin cdkOverlayOrigin
#trigger="cdkOverlayOrigin" #trigger="cdkOverlayOrigin"
[hidden]="!showSwitcher" [hidden]="!showSwitcher"
aria-haspopup="menu"
aria-expanded="isOpen"
aria-controls="cdk-overlay-container"
> >
<ng-container *ngIf="activeAccountEmail != null; else noActiveAccount"> <ng-container *ngIf="activeAccountEmail != null; else noActiveAccount">
<app-avatar <app-avatar
@ -13,6 +16,7 @@
[fontSize]="14" [fontSize]="14"
[dynamic]="true" [dynamic]="true"
*ngIf="activeAccountEmail != null" *ngIf="activeAccountEmail != null"
aria-hidden="true"
></app-avatar> ></app-avatar>
<span>{{ activeAccountEmail }}</span> <span>{{ activeAccountEmail }}</span>
</ng-container> </ng-container>
@ -24,26 +28,33 @@
aria-hidden="true" aria-hidden="true"
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-chevron-up': isOpen }" [ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-chevron-up': isOpen }"
></i> ></i>
</a> </button>
<ng-template <ng-template
cdkConnectedOverlay cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger" [cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayHasBackdrop]="true" [cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'" [cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
(backdropClick)="toggle()" (backdropClick)="close()"
(detach)="close()"
[cdkConnectedOverlayOpen]="showSwitcher && isOpen" [cdkConnectedOverlayOpen]="showSwitcher && isOpen"
[cdkConnectedOverlayPositions]="overlayPostition" [cdkConnectedOverlayPositions]="overlayPostition"
cdkConnectedOverlayMinWidth="250px" cdkConnectedOverlayMinWidth="250px"
> >
<div class="account-switcher-dropdown" [@transformPanel]="'open'"> <div
class="account-switcher-dropdown"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
>
<div class="accounts" *ngIf="numberOfAccounts > 0"> <div class="accounts" *ngIf="numberOfAccounts > 0">
<a <button
*ngFor="let a of accounts | keyvalue" *ngFor="let a of accounts | keyvalue"
class="account" class="account"
[ngClass]="{ active: a.value.profile.authenticationStatus == 'active' }" [ngClass]="{ active: a.value.profile.authenticationStatus == 'active' }"
href="#" (click)="switch(a.key)"
(mousedown)="switch(a.key)" appA11yTitle="{{ 'loggedInAsOn' | i18n: a.value.profile.email:a.value.serverUrl }}"
attr.aria-label="{{ 'switchAccount' | i18n }}"
> >
<app-avatar <app-avatar
[data]="a.value.profile.email" [data]="a.value.profile.email"
@ -52,30 +63,33 @@
[fontSize]="14" [fontSize]="14"
[dynamic]="true" [dynamic]="true"
*ngIf="a.value.profile.email != null" *ngIf="a.value.profile.email != null"
aria-hidden="true"
></app-avatar> ></app-avatar>
<div class="accountInfo"> <div class="accountInfo">
<span class="email">{{ a.value.profile.email }}</span> <span class="email" aria-hidden="true">{{ a.value.profile.email }}</span>
<span class="server" *ngIf="a.value.serverUrl != 'bitwarden.com'">{{ <span class="server" aria-hidden="true" *ngIf="a.value.serverUrl != 'bitwarden.com'">{{
a.value.serverUrl a.value.serverUrl
}}</span> }}</span>
<span class="status">{{ a.value.profile.authenticationStatus }}</span> <span class="status" aria-hidden="true">{{ a.value.profile.authenticationStatus }}</span>
</div> </div>
<i <i
class="bwi bwi-unlock bwi-2x text-muted" class="bwi bwi-unlock bwi-2x text-muted"
aria-hidden="true"
*ngIf="a.value.profile.authenticationStatus == 'unlocked'" *ngIf="a.value.profile.authenticationStatus == 'unlocked'"
></i> ></i>
<i <i
class="bwi bwi-lock bwi-2x text-muted" class="bwi bwi-lock bwi-2x text-muted"
aria-hidden="true"
*ngIf="a.value.profile.authenticationStatus == 'locked'" *ngIf="a.value.profile.authenticationStatus == 'locked'"
></i> ></i>
</a> </button>
</div> </div>
<ng-container *ngIf="activeAccountEmail != null"> <ng-container *ngIf="activeAccountEmail != null">
<div class="border" *ngIf="numberOfAccounts > 0"></div> <div class="border" *ngIf="numberOfAccounts > 0"></div>
<ng-container *ngIf="numberOfAccounts < 4"> <ng-container *ngIf="numberOfAccounts < 4">
<a class="add" routerLink="/login" (click)="addAccount()"> <button class="add" routerLink="/login" (click)="addAccount()">
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }} <i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
</a> </button>
</ng-container> </ng-container>
<ng-container *ngIf="numberOfAccounts == 4"> <ng-container *ngIf="numberOfAccounts == 4">
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span> <span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>

View File

@ -107,14 +107,18 @@ export class AccountSwitcherComponent implements OnInit {
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
} }
close() {
this.isOpen = false;
}
async switch(userId: string) { async switch(userId: string) {
this.toggle(); this.close();
this.messagingService.send("switchAccount", { userId: userId }); this.messagingService.send("switchAccount", { userId: userId });
} }
async addAccount() { async addAccount() {
this.toggle(); this.close();
await this.stateService.setActiveUser(null); await this.stateService.setActiveUser(null);
} }

View File

@ -80,6 +80,10 @@
height: 100%; height: 100%;
user-select: none; user-select: none;
border: none;
background: transparent;
width: auto;
@include themify($themes) { @include themify($themes) {
color: themed("accountSwitcherTextColor"); color: themed("accountSwitcherTextColor");
} }
@ -114,9 +118,11 @@
0 1px 5px 0 rgba(0, 0, 0, 0.2); 0 1px 5px 0 rgba(0, 0, 0, 0.2);
border-radius: $border-radius; border-radius: $border-radius;
a { button {
border: none;
background: transparent;
width: 100%;
padding: 5px 10px; padding: 5px 10px;
display: block;
@include themify($themes) { @include themify($themes) {
color: themed("textColor"); color: themed("textColor");
@ -141,6 +147,7 @@
.accountInfo { .accountInfo {
display: grid; display: grid;
text-align: left;
.email { .email {
font-size: $font-size-base; font-size: $font-size-base;
@ -173,6 +180,7 @@
.add { .add {
margin: 4px 0; margin: 4px 0;
text-align: left;
} }
.accountLimitReached { .accountLimitReached {